Wednesday, 18 December 2024

A Commodore 64 Emulator in Flutter: Part 3

Foreword

In the previous post we explored some further basics about Flutter and we looked at BloCs, state and events. In the end we wrote a very simple application for illustrating these concepts, which displayed the current epoch each time you press a button.

In this post we will work more towards our emulator. We will start to write a very simple emulator with just a few instructions implemented. With the app we will step through a machine language program and see the registers and memory dump after each instruction.

Using files in flutter

When writing an emulator on any platform, one of the first questions that comes to mind, is how do you store the binary data of all the ROM's in memory?

The quick answer is just to use a byte array, but the next question to that is how do one gets the data to populate the array. When I was creating my JavaScript emulator many years ago, in the early phases, I would just hardcore array data of 6502 machine code into the array definition.

This worked perfectly for small test programs, but as data grew to over 64 Kilobytes, suddenly this became a very messy solution, with JavaScript containing many lines of code for an array definition representing this data.

This scenario gets further dire if you want people to use your emulator where they can just slot in tape image files of their favourite games. So, it is clear that for any emulator, you need the ability to read binary files.

In the beginning of JavaScript emulator, working with binary files felt like quite a night mare for me. When you open up your index.html file for your emulator directly from your local file system, there was no way you could automatically load your ROM's from your local file system too, when your emulator starts up. The only way one could do it was with user intervention by clicking on a file button and letting him choose the file you want.

Not very intuitive. I cam to the conclusion that it is impossible to run an emulator via the local file system. The only way to automate the process of loading ROM's, was the serve the pages via a web server. You could then also make XMLHttpRequests to the server for returning the ROM's.

Using XMLHttpRequests added some further complexities in that it is asynchronous. Your JavaScript would send off a request for the data and then would continue with the rest of your code. Not so desirable if you want to wait for the data of the ROM's to load before starting your emulator.

With this past experience with JavaScript I wonder how Flutter would handle this asynchronous nature of files and decided there and then that this is the first thing I should check out before starting with serious Emulator stuff.

Looking around, I found that one can indeed add binary files to your flutter project by means of assets. So start off by creating an assets folder and dumping a binary file inside it:


Next, you need to add an entry to our pubspec.yaml file to make our project aware of our assets:


I want to stress again that this yaml file is sensitive to indentation, so make sure assets: aligns with the preceding uses-material-design attribute, and its bullet item on the following line is indented more.

Let us now add a small code snippet to our C64Bloc class for reading the first three bytes of our binary file and print it to the console:

class C64Bloc extends Bloc<C64Event, C64State< {
  C64Bloc(): super(C64State()) {
    rootBundle.load("assets/program.bin").then((value)  {
      print(value.getUint8(0));
      print(value.getUint8(1));
      print(value.getUint8(2));
    });
    on<C64Event>((event, emit) {
      emit(C64State());
    });
  }
}
The IDE will complain about rootBundle and suggest an import. Just accept the import and the error will go away.

Let us go into the bolded code into a bit more detail. As the name suggest, the load method will load our binary file. It should be noted that the load method is also an asynchronous method that will return immediately. You will also see that the method signature of the method is Future<ByteData>. ByteData is the type we want, but it is wrap in a future. You get the actual value by the then method which will only be invoked once the data is available.

You then call getUint8 to get a byte from from the data specifying the position you want as parameter. The first three bytes of the data will be written to the console. In my case it is the following:

169
21
141
This is a part of a very simple 6502 Machine language program. 169 which is Load Accumulator immediate and 141 which is Store Accumulator absolute.

Outputting data to the screen

Let us now see if we can output the data to the screen instead of the browser. This exercise will give us more inside into events and state.

First, let us see if we can structure our rootBundle.load in a more elegent way. We can indeed do this with an on<> event selector with an async selector.

So, let us implement this in our C64Bloc class. At this point our C64Block class has become a kind of a dumping ground for testing ideas. Let us cleanup this class a bit, so our whole C64Bloc class will look like this:

class C64Bloc extends Bloc<C64Event, C64State> {
  C64Bloc(): super(C64State()) {
    on<InitEmulatorEvent>((event, emit) async {
      final byteArray = await rootBundle.load("assets/program.bin");
    });
  }
}
You will see that our on selector have an async in the method signature. With this in the method signature, one can use the await when you you call another asynchronous method, which in this case is the load method.

When you use the await keyword, that line of code will wait until the asynchronous method is complete, returning the actual data, and then only continue with the next line of code.

You will also note that I have introduced another event type InitEmulatorEvent. As we go along and we need to define other different types of events, we will also define additional classes for them. However, with our Bloc class, we can only specify one type of Event Classes that we can accept, which in is case is C64Event.

The only way to get around this "limitation" would be to make all our event classes subclass C64Event and make the C64Event class itself abstract so that we don't create events by accident of C64Event type itself.

So, let us make some adjustment to the class C64Event and declare  the new InitEmulatorEvent class:

abstract class C64Event extends Equatable {
  @override

  List<Object?> get props => [];

}

class InitEmulatorEvent extends C64Event {}
Now that we have defined an on event for loading a binary file into a byteArray, the question is: How do we trigger this on selector?

This can be done by doing an add below our selector:

class C64Bloc extends Bloc<C64Event, C64State> {
  C64Bloc(): super(C64State()) {
    on<InitEmulatorEvent>((event, emit) async {
      final byteArray = await rootBundle.load("assets/program.bin");
    });
    
    add(InitEmulatorEvent());
  }
}

At this point it becomes necessary for us to have multiple states. When our emulator starts up, it should be in an initial state. When we have loaded a byteArray with our binary file, we want another state containing a dump of the data which our widget will use to display it.

So, lets rework our states a bit:

abstract class C64State extends Equatable {
  final int time = DateTime.now().millisecondsSinceEpoch;

  @override
  List<Object?> get props => [];
}

class InitialState extends C64State {}

class DataShowState extends C64State {
  DataShowState({required this.mem0, required this.mem1, required this.mem2});

  final int mem0;
  final int mem1;
  final int mem2;

  @override
  List<Object> get props => [mem0, mem1, mem2];
}
Again, we have made C64State abstract so that we don't by accident create an instance of this class. This implies of course that in our Bloc we should say C64Bloc: super(InitialState()).

In DataShowState there is quite a bit going on, so lets break it down a bit. The constructor have a number of required parameters. This is basically the parameters we should pass when we create an instance of this state.

Also, get props, have mem0, mem1 and mem2 as props. This just makes it easy to decide for flutter when we submit a new state do decide if it is different and therefore force a redraw of the widget.

We are now ready to emit some data for display to the front end. First, we need to emit the state in our selector:

    on<InitEmulatorEvent>((event, emit) async {
      final byteArray = await rootBundle.load("assets/program.bin");
      emit(DataShowState(mem0: byteArray.getUint8(0),
          mem1: byteArray.getUint8(1),
          mem2: byteArray.getUint8(2)));
    });
Here it just shows us again how nice await works and we can just use the data loaded from the previous line without worrying about jumping through other asynchronous hoops.

Now, we can actually use the data in this state in our BlocBuilder block of our widget:

...
        body: BlocBuilder<C64Bloc, C64State>(
          builder: (BuildContext context, state) {
            if (state is InitialState) {
              return const CircularProgressIndicator();
            } else if (state is DataShowState) {
              return Text(
                  '${state.mem0.toString()} ${state.mem1.toString()} ${state.mem2.toString()} ');
            } else {
              return const CircularProgressIndicator();
            }
          },
        ),
...
Within our widget, we are only interested when our BloC is in state DataShowState. For all other states we will just show a circling Progress Indicator.

The screen will render as follows:


So, we managed to render the data to the screen instead of the console!

First steps towards the emulator

Now that we have discovered how to read a binary file in flutter and displaying some of the contents on the screen, let us now start to implement the first things in our emulator.

First let us create a class for our memory in a file called memory.dart:

import 'dart:typed_data' as type_data;

class Memory {
  late type_data.ByteData _data;

  populateMem(type_data.ByteData block) {
    _data = block;
  }

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

  int getMem(int address) {
    return _data.getUint8(address);
  }

}

Here I have a method called populateMem that we will call from the outside to set memory with the binary file we loaded.

Also, we have the usual methods we expect from any memory class, to get the data from a location and setting data to a location.

Next, let us create an outline for our Cpu in a file called cpu.dart:

import 'dart:typed_data';

import 'memory.dart';

class Cpu {
  final Memory memory;
  int _a = 0, _x = 0, _y = 0;
  int pc = 0;
  Cpu({required this.memory});

  int getAcc() {
    return _a;
  }

  int getX() {
    return _x;
  }

  int getY() {
    return _y;
  }

  step() {
  ...
  }
}
Here we add the usual registers you find in a 6502, which is a, x, y and the program counter. We also pass it an instance from the Memory class we have created previously, so that the cpu can load its instructions from memory.

We also have a step method which will evolve as we go along.

I have also implemented getters for the registers, so we can display them on the screen while stepping.

With our memory and Cpu created, lets instantiate them in our BloC:

...
class C64Bloc extends Bloc<C64Event, C64State> {
  final Memory memory = Memory();
  late final Cpu _cpu = Cpu(memory: memory);

  C64Bloc() : super(InitialState()) {
    on<InitEmulatorEvent>((event, emit) async {
      final byteArray = await rootBundle.load("assets/program.bin");
      memory.populateMem(byteArray);
...
      });
...
  }
}
...
With everything initialised, it would be nice to show a dump of the registers on screen, as well as the first 256 bytes of memory as a dump. For this we need to modify our DataShowState a bit:

class DataShowState extends C64State {
  DataShowState(
      {required this.memorySnippet,
      required this.a,
      required this.x,
      required this.y,
      required this.pc});

  final ByteData memorySnippet;
  final int a;
  final int x;
  final int y;
  final int pc;

  @override
  List<Object> get props => [...];
}

So, this state now stores the value of the a, x, y and pc register, as well as a small snippet of memory. We still need to think what we are going to use for our get props, which Flutter uses to determine if one state object is different than another. I tried using object reference (e.g. this), but I got a stack overflow when everything runs. It turns out that you should never use this that extends Equatable. Equtable itself overrides the == operator. So, if you use this for get props, it will eventually use the == operator, which will cause it to call itself again, becuase of the overide, which will lead to recursion.

In the end it would be easier to use an increasing number in the state for checking if a state is different from another one. So, we add the following code:
class DataShowState extends C64State {
  DataShowState(
      {...
      required this.dumpNo});

...
      final int dumpNo;

  @override
  List<Object> get props => [dumpNo];
}


With our DataShowState been retrofitted a bit, we can push a state update when memory has been populated:

    on<InitEmulatorEvent>((event, emit) async {
      final byteArray = await rootBundle.load("assets/program.bin");
      memory.populateMem(byteArray);
      emit(DataShowState(          
          dumpNo: dumpNo++
          memorySnippet: type_data.ByteData.sublistView(byteArray, 0, 256),
          a: _cpu.getAcc(),
          x: _cpu.getX(),
          y: _cpu.getY(),
          pc: _cpu.pc));
    });

For memory snippet we just take the first 256 bytes of what we loaded as a snippet.

You will also see we do a dumpNo++. Every DataShowState we emit, we need to increase this variable, which we also defined within our BloC 

Moving towards our widget, we add two methods to MyHomePage, which will give some meaningful strings to display from the state:

  String getRegisterDump(int a, int x, int y, int pc) {
    return 'A: ${a.toRadixString(16)
        .padLeft(2, '0')
        .toUpperCase()} X: ${x.toRadixString(16)
        .padLeft(2, '0')
        .toUpperCase()} Y: ${y.toRadixString(16)
        .padLeft(2, '0')
        .toUpperCase()} PC: ${pc.toRadixString(16)
        .padLeft(4, '0')
        .toUpperCase()}';
  }

  String getMemDump(type_data.ByteData memDump) {
    String result = '';
    for (int i = 0; i < memDump.lengthInBytes; i++) {
      if ((i % 16) == 0) {
        String addressLabel = i.toRadixString(16).padLeft(4, '0').toUpperCase();
        result = '$result\n$addressLabel';
      }
      result =
      '$result ${memDump.getUint8(i).toRadixString(16)
          .padLeft(2, '0')
          .toUpperCase()}';
    }
    return result;
  }

The first function will give us all the content of the registers in a row in hexadecimal.

The second function will give a dump of our memory in a typical hexdump, 16 bytes in a row, and the address on the left side.

With our dump functions defined, let us change our widget to make use of them:

...
        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.pc)
                  ),
                  Text(
                    getMemDump(state.memorySnippet),
                  ),
                ],
              );
            } else {
              return const CircularProgressIndicator();
            }
          },
        ),
...
As you can see, we are wrapping our two Text components for the register and memory dump into a column, with the one component underneath the other. This will render as follows:


The bytes in our memory dump long line up so great, but we will fix it a bit later.

Adding the first instructions

We are now ready to add our first instructions to our Cpu. These instructions will be Load Accumulator immediate and Store accumulator Absolute.

Add the following method to the cpu.dart:

    step() {
      var opCode = memory.getMem(pc);
      pc++;
      switch (opCode) {
        case 0xa9:
          _a = memory.getMem(pc);
          pc++;
        case 0x8d:
          var arg1 = memory.getMem(pc);
          pc++;
          var arg2 = memory.getMem(pc);
          pc++;
          memory.setMem(_a, (arg2 << 8) | arg1);
      }
    }

So, here we have two selectors for our instructions LDA# (i.e. opcode A9) and STA (i.e. opcode 8D).

Now within our BloC, we also need to define an event which will be triggered when we press a button for performing a step:

  C64Bloc() : super(C64Initial()) {
...
    on<StepEvent>((event, emit) {
      _cpu.step();
      emit(C64DebugState(
          dumpNo: dumpNo++,
          memorySnippet:
              ByteData.sublistView(memory.getDebugSnippet(), 0, 256),
      a: _cpu.getAcc(), x: _cpu.getX(), y: _cpu.getY(), pc: _cpu.pc));
    });
...
  }

Finally, we need to modify our floating action button, which we defined in the previous post, to trigger a step event on each click:

        floatingActionButton: FloatingActionButton(
          tooltip: 'Step',
          onPressed: () {
            context.read<C64Bloc>().add(StepEvent());
          },
          child: const Icon(Icons.arrow_forward),
        ));

The Results

So with all code writen, let us give our Flutter program another test run.

Our app starts up with the following screen:


So, with the initial state, we have the program loaded in memory and the registers are zero. People that are familiar with the 6502 CPU will know that the CPU doesn't start executing at address 0 out of reset, but rather look for an initial location from the reset vector at locations 0xfffc and 0xfffd. We will eventually get there in later posts, but for now we are just keeping things simple, and just start executing at location 0.

Next, let us click the step button, lower right and see what happens.

As highlighted, we see two things happened to our registers. Accumulator has been loaded by value 0x15, and our Program counter has updated to value 2, which points to a store accumulator instruction.

Lets click the step button again:

This time we see that the value of the accumulator, 0x15, have been stored at memory location 0x20.

So, we have successfully implemented two instructions in our accumulator!

Viewing the dump in Mono Space

As seen, with our memory dump, the byte values doesn't line up so nicely underneath each other.

To fix this, we need to download a mono space font and add it to our project. So head to the following URL:

https://fonts.google.com/specimen/Roboto+Mono

Then click "Get Font" and then "Download All".

You then need to open the file the zip and extract the file RobotoMono-VariableFont_wght.ttf to the folder fonts folder of your project. The fonts folder should thus be on the same level as your assets folder.

Next, we should edit our pubspec.yaml file again. Open it, and below the assets section add the following section:

  fonts:
    - family: RobotoMono
      fonts:
        - asset: fonts/RobotoMono-VariableFont_wght.ttf
          weight: 700
Now, we need to add a style element to the Text which displays the memory dump:

                  Text(
                    getMemDump(state.memorySnippet),
                    style: const TextStyle(
                      fontFamily: 'RobotoMono', // Use the monospace font
                    ),
                  ),

With this the dump looks better:


In Summary

In this post we created a very simple emulator in Flutter that executed two types instructions, LDA and STA.

In the next post we will continue to evolve our emulator by adding the different address modes.

Till next time!

No comments:

Post a Comment