Foreword
Using files in flutter
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.
169 21 141This 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
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.
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?
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.
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()).
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.
... 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.
First steps towards the emulator
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.
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.
... 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.
class DataShowState extends C64State { DataShowState( {... required this.dumpNo}); ... final int dumpNo; @override List<Object> get props => [dumpNo]; }
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.
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.
... 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:
Adding the first instructions
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).
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
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.
This time we see that the value of the accumulator, 0x15, have been stored at memory location 0x20.
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
fonts: - family: RobotoMono fonts: - asset: fonts/RobotoMono-VariableFont_wght.ttf weight: 700Now, 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: