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:
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.
... 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.
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
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.
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.
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:
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.
Rendering screen memory
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.
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.
... 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.
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.
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.
Getting the cursor to flash
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: