Foreword
In the previous post we managed to emulate the rugged edges of the flashing borders when loading the game Dan Dare from a tape image in our C64 Flutter Emulator.
We achieved this by queuing border color change on a scanline and rendering these changes at the end of the scan line.
Now, while the game Dan Dare loads from a tape image, apart from the flashing borders, it also display a splash image.
In this post we will see if we can implement the splash image.
I often forget to mention it in these posts of my Flutter emulator, that the source code for each post is available on GitHub as a tag. The source code for this post, can be found here.
Enjoy!
Snooping
I mentioned in the rpevious section, that the purpose of this post is to load show loading splash screen. In these old C64 games, these high resolution splash screen images was one of the exciting parts of loading the game.
However, looking at our C64 emulator, at the screen where we are supposed to see the splash screen, we see something far less exciting:
No Hired graphics, just standard boring text mode.
A good place to start is to snoop around in the VIC-II registers at this phase of the loading, to see in what mode the graphics should be.
To do this, strt the emulator within in IntelliJ in debug mode. As soon as you get to the screen above, but a breakpoint at the following line.
Now we can inspect a couple of registers inside _regs. The first thing we can inspect is in what screen mode the VIC-II is. This is set in memory location D011, i.e. register 0x11 in _regs.
when we evaluate, we get 59 decimal. Covert this to hexadecimal you get 3b. This means bit 5 is set, telling us high resolution mode is enabled.
Next, it would be interesting to find out where in memory, this display data is stored. To start this investigation, lets pause a bit, and ponder on how the VIC-II sees memory.
In a previous post, I breiefly mentioned that the VIC-II has a 14-bit address, which can only access 16KB. To access the full 64kb range, The CIA2 chip serve the remaining 2 bits to give you that full 16 bit address bus.
These two bits are served by the lowest 2 bits of register DD00, inverted. This divide the 64KB space into 4 banks of 16KB each. The bank schema with these 2 bits of DD00 is as follows:
- %00, 0: Bank #3, $C000-$FFFF, 49152-65535.
- %01, 1: Bank #2, $8000-$BFFF, 32768-49151.
- %10, 2: Bank #1, $4000-$7FFF, 16384-32767.
- %11, 3: Bank #0, $0000-$3FFF, 0-16383.
Now let us do some snooping of DD00 to see how the VIC-II bank number changes when the splash screen shows.
As a baseline, lets see the value of register DD00 with a fresh start up. For this exercise, we start the emualtor in debug mode, and when we see the welcome screen we put a breakpoint in getMem() of memory.dart.
This register evaluate as 23 decimal, which is 17 Hex. This implies the lowest two bits are set, which resolves to bank 0, e.g. 0 - 16383. This is correct for startup, seeing that in this configuration, screen memory is within address 1024 - 2047.
Now let us do the inspection when the splash screen show:
This time, DD00 evaluates 4, meaning botht the lower two bits are zero. Doing the inversion we get to 11, which is the last page of memory at C000-FFFF.
Next, it would be interesting to see where the display info is stored within this memory bank. Register D018 is commissioned for this purpose:
- Bit 7 - 4: Bit 13 to 10 of the VIC-II address bus, for screen memory accesses.
- Bit 3 - 1: Bit 13 to 11 of the VIC-II address bus, for character data access. When in bitmap mode, only bit 13 is used, so within a bank the bitmap data can either be within 0 - 7999, or from 8192 - 16191.
Now when we are i the phase the splash screen should show, we evaluate D018 as 8. With everything we discussed, with this the screen memory resolve to address C000 and Bitmap memory as E000.
If one carefully inspect these areas during loading, you will find the memory area at C000 starts populating before the splah screen opens. Also as soon as the splash screen open, you can see the memory area at E000 gets populated.
Implementing VIC-II memory mapping
In the previous section, we gathered a lot of info regarding how the VIC-II maps display data into memory. In this section, we will implement all that in code.
Firstly, we will look at our Memory class. We add the following global variables:
... var vicBank = 0; final type_data.ByteData _colorRam = type_data.ByteData(1024); ...The vicBank variable is a mirror of the lower two bits of the register DD00.
_colorRam is exactly what it says. This is a data structure we have avoided up to now, since in standard operation the value of this structure doesn't really change. However, we are now moving to a point where this is not the case any more.
Next, we make some changes to our setMem() method:
setMem(int value, int address ) {
var addressHigh = address >> 8;
var addressLow = address & 0xff;
if (addressHigh == 0xDC) {
cia1.setMem(address, value);
} else if (addressHigh == 0xD0) {
vic.setReg(address, value);
} else if (address >= 0xD800 && address <= 0xDBFF) {
_colorRam.setInt8(address & 0x3FF, value);
} else if (address == 0xDD00) {
vicBank = value & 3;
} else if (address == 1) {
_ram.setInt8(address, value);
_tape.setMotor((value & 0x20) == 0 );
_kernelEnabled = (value & 2) != 0;
} else {
_ram.setInt8(address, value);
}
}
We just added functionality for setting Color RAM and vicBank.Similarly, we make changes to the getMem() method:
int getMem(int address) {
_readCount++;
if (address >= 0xA000 && address <= 0xBFFF) {
return _basic.getUint8(address & 0x1fff);
} else if (address >= 0xE000 && address <= 0xFFFF && _kernelEnabled) {
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 >= 0xD800 && address <= 0xDBFF) {
return _colorRam.getInt8(address & 0x3FF);
} else if (address == 0xDD00) {
return vicBank;
} else if (address == 1) {
var value = _ram.getUint8(address) & 0xef;
return value | _tape.getCassetteSense();
} else {
return _ram.getUint8(address);
}
}
Next, in this class, we make some modifications in the method for reading from memory for the VIC:We have implemented something similar here than a physical VIC-II chip. on a real VIC-II chip the data bus is not 8 bits, but 12 bits. Four of the bits is part of data coming from the Color RAM.(type_data.Uint8List, type_data.Uint8List) readVicRange(int address, int count) {var (storage, resolvedAddress) = _resolveVicAddress(address); return (storage.buffer.asUint8List(resolvedAddress, count), _colorRam.buffer.asUint8List(address & 0x3FF, count)); }
There is one final method on the Memory class we modify:
(type_data.ByteData, int) _resolveVicAddress(address) {
address = address | ((~vicBank & 3) << 14);
if (address >= 0x1000 && ((vicBank & 1) == 1)) {
return (_character, address & 0xfff);
}
return (_ram, address);
}
Here we use the vicBank to assemble the final resolved VIC-II address. Also, the extra extra check added, vicBank & 1, we only return content of character ROM at address 0x1000, with even banks 0 and 2. This is how it works on a real C64.Rendering Bitmap Mode
Let us now move onto rendering bitmap mode.
Within our VicII class, we move down to the following section and do some changes as bolded:
if (visibleVerticalRegion && displayEnabled) {
if (charLine == 0) {
var screenMemBase = (_regs.getInt8(0x18) & 0xf0) << 6;
var result = memory.readVicRange(videoMatrixPos | screenMemBase, 40);
_charCodeBuffer = result.$1;
_colorRamBuffer = result.$2;
}
Here we get a line worth of character codes of screen memory and color codes from color RAM. With the address request send to our Memory class, we properly resolve the relative address within the 16KB bank via register 0x18, as we discussed in an ealrier section.
The $1 and $2 are the tuple syntax for Dart in Flutter, giving us a row of Character codes and Color codes.
Next we scroll down further in the method of drawScanLine(), where we actually draw the character data, moving the existing functionality into a method of its own:
var bitmapMode = (_regs.getInt8(0x11) & 0x20) != 0;
if (!bitmapMode) {
drawStandardTextLine();
} else if (bitmapMode) {
drawBitmapModeLine();
}
Here we first determine if we are in bitmap mode as discussed previously, and either render standard text mode or bitmap mode.Lets quickly look how the extracted method drawStandardTextLine() looks like, which contain some of our old code:
void drawStandardTextLine() {
var charDrawPointer = currentPosStartLine + 40;
for (int i = 0; i < _charCodeBuffer.length; i++) {
int charCode = _charCodeBuffer[i];
int colorCode = _colorRamBuffer[i];
var bitmapRow = memory.readVic((charCode << 3) | charLine | 0x1000);
if (bitmapRow & 0x80 != 0) {
c64Buffer[charDrawPointer] = colorCode;
}
if (bitmapRow & 0x40 != 0) {
c64Buffer[charDrawPointer + 1] = colorCode;
}
if (bitmapRow & 0x20 != 0) {
c64Buffer[charDrawPointer + 2] = colorCode;
}
if (bitmapRow & 0x10 != 0) {
c64Buffer[charDrawPointer + 3] = colorCode;
}
if (bitmapRow & 0x08 != 0) {
c64Buffer[charDrawPointer + 4] = colorCode;
}
if (bitmapRow & 0x04 != 0) {
c64Buffer[charDrawPointer + 5] = colorCode;
}
if (bitmapRow & 0x02 != 0) {
c64Buffer[charDrawPointer + 6] = colorCode;
}
if (bitmapRow & 0x01 != 0) {
c64Buffer[charDrawPointer + 7] = colorCode;
}
charDrawPointer = charDrawPointer + 8;
}
}
This is the same code more or less than our old except that we don't hard code the foreground color of Light Blue (14) anymore. We now properly get the color code from color RAM.Next, let us have a look at how to render a line of graphics in Bitmap Mode:
void drawBitmapModeLine () {
var charDrawPointer = currentPosStartLine + 40;
var bitmapOffset = (_regs.getInt8(0x18) & 8) == 8 ? 0x2000 : 0;
for (int i = 0, currentBlock = videoMatrixPos; i < _charCodeBuffer.length; i++, currentBlock++) {
int charCode = _charCodeBuffer[i];
int foregroundColor = charCode >> 4;
int backgroundColor = charCode & 0xf;
var bitmapRow = memory.readVic((currentBlock << 3) | charLine | bitmapOffset);
c64Buffer[charDrawPointer] = (bitmapRow & 0x80 != 0) ? foregroundColor : backgroundColor;
c64Buffer[charDrawPointer + 1] = (bitmapRow & 0x40 != 0) ? foregroundColor : backgroundColor;
c64Buffer[charDrawPointer + 2] = (bitmapRow & 0x20 != 0) ? foregroundColor : backgroundColor;
c64Buffer[charDrawPointer + 3] = (bitmapRow & 0x10 != 0) ? foregroundColor : backgroundColor;
c64Buffer[charDrawPointer + 4] = (bitmapRow & 0x8 != 0) ? foregroundColor : backgroundColor;
c64Buffer[charDrawPointer + 5] = (bitmapRow & 0x4 != 0) ? foregroundColor : backgroundColor;
c64Buffer[charDrawPointer + 6] = (bitmapRow & 0x2 != 0) ? foregroundColor : backgroundColor;
c64Buffer[charDrawPointer + 7] = (bitmapRow & 0x1 != 0) ? foregroundColor : backgroundColor;
charDrawPointer = charDrawPointer + 8;
}
}
You will notice that the mechanism for retrieving bitmap data is almost the same mechanism as fetching data from Character ROM when rendering a line in Standard Text Mode. The only difference is that we don't use a character code as an index for fetching the data, but rather the current position in Screen memory act as the index for fetching the bitmap data.Talking of screen memory in bitmap mode. You will also notice that the content of screen memory have a totally different function in bitmap mode. In bitmap mode every location of screen memory, the upper 4 bits represents the foreground color, and the lower 4 bits the background color of the 8x8 pixel area it represents.
You will also notice that bitmapOffset represents something we discussed earlier. Within the 16KB bank the bitmap data either resides in the upper 8KB or the lower 8KB.
Attempt #1
With Bitmap Mode implemented, lets see how the splash renderes:
Doing some further snooping of the VIC-II registers, I find something interesting in register D016. This register evaluates to -40, which is D8. This means bit 4 is set. This means, surprise, surprise, multicolor mode is enabled with the splash screen!
So, we are in the wrong bitmap mode, and we need to be in multicolor bitmap mode. We haven't implemented this mode yet, and will be implemented in the following section.
Implementing Multicolor Bitmap Mode
Let us now look at implementing Multicolor Bitmap Mode. First we add a selecter for this mode:
var bitmapMode = (_regs.getInt8(0x11) & 0x20) != 0;
var multiColorMode = (_regs.getInt8(0x16) & 0x10) != 0;
if (!bitmapMode) {
drawStandardTextLine();
} else if (bitmapMode && !multiColorMode) {
drawBitmapModeLine();
} else if (bitmapMode && multiColorMode) {
drawBitmapMcmModeLine();
}
Now, every pixel in this mode is two bits wide. Each one of the four values for each two bits, represents the following:- 00: Uses the Global Background Color from VIC-II register $D021. This color is shared across the entire screen.
- 01: Uses Color 1, defined by the upper 4 bits (nibble) of the corresponding byte in Screen RAM ($0400–07FF$).
- 10: Uses Color 2, defined by the lower 4 bits (nibble) of the corresponding byte in Screen RAM ($0400–07FF$).
- 11: Uses Color 3, which comes from the corresponding byte in Color RAM ($D800–DBFF$).
Next we implement this in code:
void drawBitmapMcmModeLine () {
var charDrawPointer = currentPosStartLine + 40;
var bitmapOffset = (_regs.getInt8(0x18) & 8) == 8 ? 0x2000 : 0;
// %00 uses the global background color register ($D021)
int globalBgColor = _regs.getInt8(0x21) & 0x0f;
for (int i = 0, currentBlock = videoMatrixPos; i < _charCodeBuffer.length; i++, currentBlock++) {
int charCode = _charCodeBuffer[i];
// Extract the two nibbles from Screen RAM for this 8x8 block
int color0 = charCode >> 4; // High nibble -> maps to code %01
int color1 = charCode & 0x0f; // Low nibble -> maps to code %10
int color2 = _colorRamBuffer[i];
var bitmapRow = memory.readVic((currentBlock << 3) | charLine | bitmapOffset);
int resolvedColor;
// Pair 1 (Bits 7-6)
resolvedColor = _resolveMultiColorColor((bitmapRow >> 6) & 0x03, globalBgColor, color0, color1, color2);
c64Buffer[charDrawPointer] = resolvedColor;
c64Buffer[charDrawPointer + 1] = resolvedColor;
// Pair 2 (Bits 5-4)
resolvedColor = _resolveMultiColorColor((bitmapRow >> 4) & 0x03, globalBgColor, color0, color1, color2);
c64Buffer[charDrawPointer + 2] = resolvedColor;
c64Buffer[charDrawPointer + 3] = resolvedColor;
// Pair 3 (Bits 3-2)
resolvedColor = _resolveMultiColorColor((bitmapRow >> 2) & 0x03, globalBgColor, color0, color1, color2);
c64Buffer[charDrawPointer + 4] = resolvedColor;
c64Buffer[charDrawPointer + 5] = resolvedColor;
// Pair 4 (Bits 1-0)
resolvedColor = _resolveMultiColorColor((bitmapRow >> 0) & 0x03, globalBgColor, color0, color1, color2);
c64Buffer[charDrawPointer + 6] = resolvedColor;
c64Buffer[charDrawPointer + 7] = resolvedColor;
charDrawPointer = charDrawPointer + 8;
}
}
int _resolveMultiColorColor(int pair, int bg0, int c0, int c1, int c2) {
switch (pair) {
case 0: return bg0; // %00: Global background ($D021)
case 1: return c0; // %01: Screen RAM high nibble
case 2: return c1; // %10: Screen RAM low nibble
case 3: return c2; // %11: Color RAM lower nibble
default: return 0;
}
}
For each two pair, we have the method _resolveMultiColorColor, which resolve it to the actual color of the pixel.With this implemented let us see how the splash screen renders this time:
Now as the loading continues, I eventually get to a very strange screen, where everything appears to be hanging:
This is due to bank switching I haven't implemented in full. We will tackle this in the next post.
In Summary
In this post we implemented the two high resolution modes in order to see the rendered splash screen during loading.
In the next post, we will be implementing bank swicthing in full.
Till next time!



