Foreword
In the previous post, we fixed a memory leak in our emulator. It was fixed by changing the emulator to use a HTML Canvas, and reusing the same canvas instance with every frame.
In this post we will be implementing proper border rendering, so that we can properly emulate the flashing borders while we load the game Dan Dare from a tape image.
In order to achieve this we will start to implement the VICII in our emulator with its registers in this post.
Implementing the VICII class
Lets start our discussion, by creating an outline for our VICII class:
class Vicii {
final type_data.ByteData _regs = type_data.ByteData(0x50);
int getReg(int address) {
return _regs.getUint8(address & 0x3f);
}
setReg(int address, int value) {
_regs.setInt8(address & 0x3f, value);
}
}
We have declared 80 local registers for our VICII class. Also, we have created getReg() and setReg() registers so our Memory class can alter the contents of the registers. 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 if (address == 0xD012) {
return (_readCount & 1024) == 0 ? 1 : 0;
} else if ((address >> 8) == 0xDC ) {
return cia1.getMem(address);
} else if ((address >> 8) == 0xD0) {
return vic.getReg(address);
} else if (address == 1) {
var value = _ram.getUint8(address) & 0xef;
return value | _tape.getCassetteSense();
} else {
return _ram.getUint8(address);
}
}
setMem(int value, int address ) {
if ((address >> 8) == 0xDC) {
cia1.setMem(address, value);
} else if ((address >> 8) == 0xD0) {
vic.setReg(address, value);
} else if (address == 1) {
_ram.setInt8(address, value);
_tape.setMotor((value & 0x20) == 0 );
} else {
_ram.setInt8(address, value);
}
}
Now, let us see how the VICII class fit within our emulator:class EmulatorController implements KeyInfo{
final Memory memory = Memory();
final Vicii vic = Vicii();
...
Future<void> _init() 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");
Cia1 cia1 = Cia1(alarms: alarms);
cia1.setKeyInfo(this);
Tape tape = Tape(alarms: alarms, interrupt: cia1);
_tape = tape;
memory.setCia1(cia1);
memory.populateMem(basicData, characterData, kernalData);
memory.setTape(tape);
vic.memory = memory;
memory.vic = vic;
_cpu.setInterruptCallback(() => cia1.hasInterrupts());
_cpu.reset();
}
...
}
So, basically the VICII instance and the memory instance have a reference to each other. The VIC instance needs a reference to memory because it needs access to screen memory and bitmapped graphics. int readVic(int address) {
if (address >= 0x1000 && address < 0x2000) {
return _character.getUint8(address & 0xfff);
}
return _ram.getUint8(address);
}
Now, you will remember that previous in the memory class, we had a method renderDisplayImage(), where we rendered the contents of a C64 frame to a bytebuffer, which wrote to a HTML Canvas. We also need to move this method to our VicII class, which will handle all the frame rendering:class Vicii {
final type_data.ByteData _regs = type_data.ByteData(0x50);
late final Memory memory;
late type_data.Uint32List image;
...
void renderDisplayImage() {
const rowSpan = 320;
for (int i = 0; i < 1000; i++) {
var charCode = memory.readVic(i + 1024);
var charAddress = charCode << 3;
var charBitmapRow = (i ~/ 40) << 3;
var charBitmapCol = (i % 40) << 3;
int rawPixelPos = charBitmapRow * rowSpan + charBitmapCol;
for (int row = 0; row < 8; row++) {
int bitmapRow = memory.readVic((row + charAddress) | 0x1000) : 0;
int currentRowAddress = rawPixelPos + row * rowSpan;
for (int pixel = 0; pixel < 8; pixel++) {
if ((bitmapRow & 0x80) != 0) {
image[currentRowAddress + (pixel)] = 0x000000ff;
} else {
image[currentRowAddress + (pixel)] = 0xffffffff;
}
bitmapRow = bitmapRow << 1;
}
}
}
}
}
You will also note that to get the bitmap data, we OR it with 0x1000, so that our memory class will know to get the data from the character ROM.Working with scan lines
class Vicii {
...
final type_data.Uint8List c64Buffer = type_data.Uint8List(400*284);
...
}
I have rounded off the horizontal resolution, just to keep things simple. You will see also that I use a buffer of bytes, instead of 32-bit integers. Instead, we will be working with 4 bit color values in each byte, which is an index to a color palette. In rendering each scanline, there is multiple writes to the same pixel, like writing the background, then the foreground, and potentially drawing sprites as well. This volume of data is just reduced using 4-bit entries instead of the 32-bit entries.class Vicii {
...
Vicii(Alarms alarms) {
_alarms = alarms;
setupAlarms();
}
...
setupAlarms() {
_vicAlarm ??= _alarms.addAlarm( (remaining) => processVicAlarm(remaining));
_vicAlarm!.setTicks(63);
}
...
processVicAlarm(int remaining) {
_vicAlarm!.setTicks(63 + remaining);
}
...
}
We added a constructor for our Vicii class where we pass in the alarms structure, which the Vicii class basically add itself as an extra alarm. processVicAlarm(int remaining) {
_vicAlarm!.setTicks(63 + remaining);
if (yReg >= 17 && yReg <= 300) {
drawScanLine();
}
yReg++;
if (yReg == 312) {
yReg = 0;
}
}
yReg is a raster counter we have implemented. As from Bauer's document, it counts from 0 to 312. The counter also counts during vertical blanking period. Between counts 17 and 300 is where there is visible lines. This is where we call drawScanline. void drawScanLine() {
int borderColor = _regs.getInt8(0x20);
int backgroundColor = _regs.getInt8(0x21);
// process full border
if (yReg < 51) {
c64Buffer.fillRange(currentPosStartLine, currentPosStartLine + 400, borderColor);
}
currentPosStartLine = currentPosStartLine + 400;
}
We start by getting the borderColor and Background. The, if the raster counter is less than 51 we draw a full line in the border color. Raster line 51 is the last line of the top border region, so with the if statment we draw the complete top border region. void drawScanLine() {
int borderColor = _regs.getInt8(0x20);
int backgroundColor = _regs.getInt8(0x21);
// process full border
if (yReg < 51) {
c64Buffer.fillRange(currentPosStartLine, currentPosStartLine + 400, borderColor);
}
visibleVerticalRegion = yReg < 251 && yReg >= 51;
var displayEnabled = (_regs.getUint8(0x11) & 0x10) != 0 ? true : false;
if (visibleVerticalRegion && displayEnabled) {
c64Buffer.fillRange(currentPosStartLine, currentPosStartLine + 40, borderColor);
c64Buffer.fillRange(currentPosStartLine + 40, currentPosStartLine + 40 + 320, backgroundColor);
c64Buffer.fillRange(currentPosStartLine + 40 + 320, currentPosStartLine + 40 + 320 + 40, borderColor);
} else {
// process full border
c64Buffer.fillRange(currentPosStartLine, currentPosStartLine + 400, borderColor);
}
currentPosStartLine = currentPosStartLine + 400;
}
So, basically, when we are in the region where the characters are drawn, we just draw a 40 pixel border on the left and on the right, and fill the middle part with the background color. If, however, the display is blanked, we will fill the whole scanline in the border color.Drawing the characters on the scan line
if (visibleVerticalRegion && displayEnabled) {
// process visible screen line
if (charLine == 0) {
_charCodeBuffer = memory.readVicRange(videoMatrixPos | 1024, 40);
}
...
} else {
// process full border
c64Buffer.fillRange(currentPosStartLine, currentPosStartLine + 400, borderColor);
}
if (visibleVerticalRegion) {
charLine++;
charLine = charLine & 7;
if (charLine == 0) {
videoMatrixPos = videoMatrixPos + 40;
}
}
Firstly, we have a variable charLine, which we increment with every scanline in the character region. This indicates which line of a character (lines 0 to 7) we are busy with. With this variable we also control another variable, videoMatrixPos, which tells us any point in time at which row we are in screen code memory. type_data.Uint8List readVicRange(int address, int count) {
return storage.buffer.asUint8List(address, count);
}
Here Flutter helps us out a bit by providing us with the oprator asUint8List on a ByteBuffer, where we specify an offset address and a number of bytes to return from that address. So, we can now read 40 characters of screen memory at the beginning of each character row. if (visibleVerticalRegion && displayEnabled) {
// process visible screen line
if (charLine == 0) {
_charCodeBuffer = memory.readVicRange(videoMatrixPos | 1024, 40);
}
c64Buffer.fillRange(currentPosStartLine, currentPosStartLine + 40, borderColor);
c64Buffer.fillRange(currentPosStartLine + 40, currentPosStartLine + 40 + 320, backgroundColor);
c64Buffer.fillRange(currentPosStartLine + 40 + 320, currentPosStartLine + 40 + 320 + 40, borderColor);
var charDrawPointer = currentPosStartLine + 40;
for (var charCode in _charCodeBuffer) {
var bitmapRow = memory.readVic((charCode << 3) | charLine | 0x1000);
if (bitmapRow & 0x80 != 0) {
c64Buffer[charDrawPointer] = 14;
}
if (bitmapRow & 0x40 != 0) {
c64Buffer[charDrawPointer + 1] = 14;
}
if (bitmapRow & 0x20 != 0) {
c64Buffer[charDrawPointer + 2] = 14;
}
if (bitmapRow & 0x10 != 0) {
c64Buffer[charDrawPointer + 3] = 14;
}
if (bitmapRow & 0x08 != 0) {
c64Buffer[charDrawPointer + 4] = 14;
}
if (bitmapRow & 0x04 != 0) {
c64Buffer[charDrawPointer + 5] = 14;
}
if (bitmapRow & 0x02 != 0) {
c64Buffer[charDrawPointer + 6] = 14;
}
if (bitmapRow & 0x01 != 0) {
c64Buffer[charDrawPointer + 7] = 14;
}
charDrawPointer = charDrawPointer + 8;
}
} else {
// process full border
c64Buffer.fillRange(currentPosStartLine, currentPosStartLine + 400, borderColor);
}
You will see that I have introduced a variable charDrawPointer. This points to the beginning of the character area on the line, and not the beginning of the border area.Testing everything
static const List<int> c64Colors = [ 0xFF000000, // Black 0xFFFFFFFF, // White 0xFF000088, // Red 0xFFEEFFAA, // Cyan 0xFFCC44CC, // Purple 0xFF55CC00, // Green 0xFFAA0000, // Blue 0xFF77EEEE, // Yellow 0xFF5588DD, // Orange 0xFF004466, // Brown 0xFF7777FF, // Light Red 0xFF333333, // Dark Grey 0xFF777777, // Grey 0xFF66FFAA, // Light Green 0xFFFF8800, // Light Blue 0xFFBBBBBB, // Light Grey ];And next, the method for creating the 32-bit bitmap:
void renderDisplayImage() {
for (int i = 0; i < 400 * 284; i++) {
image[i] = c64Colors[c64Buffer[i] & 0xf];
}
}
There is some other minor changes, like not letting the main emulator loop decide when to render a frame, but letting the VIC-II call the shots for this. However, to keep the dicussion simple, I will not cover this here, but you can have a look at the source link I will provide at the end of this post, to get an idea of the finer details.








