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.We will receive the full address externally, but internally we will just look at the lower 6 bits, by adding the address with 0x3f.
Inside the Memory class we will map the VICII in our memory space as follows:
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.It should be remembered that the VICII's view of the memory is different than that of the CPU. The VICII accesses memory with an address bus that is only 14 bits wide, compared to the 16 bits address bus of the CPU.
The VIC-II can therefore only see 16KB of memory at a time. In the CIA-2 chip, there is a register that you can set telling which 16KB block should be visible within the 64KB address space to the VIC-II at any point in time.
With the above setup we will give the VIC-II banked access to the RAM. However, we will need a way way to give the VIC-II access to character ROM, so it can draw the characters of screen RAM when in Text Mode. To enable this, two of the four banks has the character ROM also mapped into the address space.
The first bank that has the character ROM mapped inside, is the one in the range 0 - 0x4000, and this range the character ROM is mapped at addresses 0x1000 - 0x1FFF. This bank is the default bank the VIC-II uses when the C64 powers up.
So, with all this in mind, let me write a method inside our memory class for for VIC-II memory access:
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.Obviously we will need to rewire parts of our emulator to make use of the Vic instance instance instead of the memory instance, which includes passing the canvas byte array instance. To keep the discussion focused, I will not be going into this detail.
Working with scan lines
We mentioned in the beginning of this post that we will be emulating the flashing borders while loading the game "Dan Dare". These flashing borders are alternating horizintal lines that changes the whole time as the border colors are adjusted a number of times per frame.
Now, the caveat is that with our current setup, we always render a whole frame at the end of emulating a whole frame worth of CPU cycles. This means that we render every frame with only a single border color, so we will never get that flashing border effect with our current setup.
To get closer to emulating flashing borders, we need to render after emulating a scanline worth of cycles every time, instead of waiting for a whole frame of CPU cycles.
To aid us in doing this closer emulation, we will look at this write-up from Christian Bauer on the VIC-II: https://www.zimmers.net/cbmpics/cbm/c64/vic-ii.txt This is a golden resource many people used for writing emulators. Even if you look at the source code for the VICE emulator, you will find reference to Christian Bauer's work.
Let us start by looking at key figures in Bauer's write up:
The model we are interested in is the PAL-B model. The first figures that are handy is the Visible Lines and Visible pixels/line. Many seasoned C64 programmers, will say off the bat that the resolution of the C64 screen is 320x200. However, when asked what is the total resolution, including the border, which you will need to know when showing a screen during emulation, few of us will have an off the bat answer.
This table provides us the answer, which is 403x284. We can now define a screen buffer for this in our VicII class:
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.It is only once we have a full frame buffer ready for display, that we will convert it to 32-bit integer buffer.
From the table above we see that with every scanline, 63 CPU cycles gets executed. This gives us a clue that every 63 clock cycles we should render a scaline with the current state of VIC-II registers. We can do this by just adding another alarm to our alarm system that we previously developed for trigger tape pulses during tape loading emulation. Here is the code for doing that:
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.We have set the alarm to trigger after 63 cycles. Once it triggers, we extend it trigger after another 63 cycles.
Let us extend the method processAlarm a bit more:
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.Lets end off this section by starting to implement drawScaline, with only the parts of drawing the border and the background. We will cover the drawing of the characters in a scan lined fashion in the next section.
Let us start by looking at a C64 screen, so we can visualise what border areas needs to be drawn:
Firstly, you get a border section right at the top. Every scanline in this section, is fully drawn with the border color.
We then move down to the area where the characters is drawn. Here every scan line a small section of the border is drawn on the left and on the far right. In the character area itself in the scan line we draw a solid line of the back ground color and set pixels to the foreground color that the bitmaps of the character bitmaps dictate.
Finally, at the bottom of the screen, after the character area, every scanline is drawn in the border color in full.
Now, let us start drawing the top part of the border:
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.We also have introduced another variable, currentPosStartLine, which always point to the start of the current scanline.
Finally, let us draw the rest of the borders:
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
Let us now focus on drawing the characters on the screen. We will also do this in a scan line fashion.
To draw a bitmap of a character, we need two memory accesses, the character code from screen memory and a line from pixels from character ROM for the character code. In our current rendering we do draw each character at the screen at once. Changing this to a scan line fashion, forces us to rethink how we pull the info from our memory structures to get the info for drawing.
Christian Bauer describes in his write up on VIC-II how the actual VIC-II chip sequences the memory access to get the raw pixel data to draw. This description is also actual handy for us in finding a software implementation for getting the data. He states that at the first scan line of every row of characters, called a bad line, you fetch all the character codes from screen memory to be drawn for that row to a 40 byte buffer resident on the VIC-II itself.
That line is called a bad line because the VIC-II needs to halt the processor from accessing the memory bus completely on that line, because the VIC-II needs extra memory bandwidth to fetch the character codes as well as the bitmap line segments from character ROM.
For the rest of the scan lines for the character row, however, the character codes are already in the 40 byte character Buffer on the VIC-II, so no extra memory bandwidth for them.
This also sounds like a nice implementation for our emulation as well. For the first line of every character row, fetch the character codes from screen memory, and store it in a 40 byte array buffer. Drawing the scan line is then straightforward, fetches the character codes from that array.
Let us start by implementing this idea of prefetching the screen codes:
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. When charLine reaches zero, it signal the beginning of the next character row. This triggers two things, the loading of the 40 character codes from screen memory, and advancing the pointer in screen memory to the next line.
To read an array of 40 characters from screen memory, we introduced a new method to our Memory class called readVicRange(), which look like as follows:
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.Next, let us focus on drawing a line of bitmap data of a character. The idea that comes to mind, is to read a byte of pixel data from Character ROM, shifting it left one bit at a time, and and then draw the pixel in the foreground color, depending on whether the current MSB bit is set or not.
However, out of pure curiosity I had a look at the VICE source code, to see how they handle this, and I was quite surprised. They use this macro to do the drawing of character pixels:
This actually motivated me to do the same in my emulation code. I know that modern day compilers does loop unrolling, and I wouldn't know how efficient it would unroll a loop where we do bit shifting and testing. Needless to say, here is my code for doing it in a loop unrolled fashion 😀:
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.Also for the time being, I am assigning a hard coded pallette entry for the pixels that gets set. At a later stage I will pull this value from color memory.
Testing everything
Let us now get ready to test our setup. One final thing we need to do is to convert the 4-bit bitmap to a 32-bit rgba bitmap.
First thing is to define the color pallette in our VIC-II class:
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.Starting our emulator up, I immediately feel at home:
Let us now see how the screen looks like when loading the game Dan Dare:
We have flashing borders! However, if you look closely, the flashing borders looks a bit unnatural, if you compare it to the how the flashing borders looked like back in the day. In, fact the summarise it, these bars looks too parallel!
Lets compare it to the loading screen on Vice itself:
Here we can clearly see the bars looks rugged in the scan lines. The reason our's doesn't look like this, s because we only use a single border color per scan line.
In the next post we will see if we can get closer to this look.
In Summary
In this post we have implemented border rendering in a scan line fashion.
We also managed to implement the flashing borders of when loading the game Dan Dare. However, the bars looks kind of artificial because we only render each scan line with one border color.
In the next post we will see if we can deal with changing border colors on a scan line and render it correctly.
The source for this post can be found here
Until next time!



