Foreword
In the previous blog we managed to successfully load the game Dan Dare, but ended up with a garbled intro screen.In this blog we will investigate why the intro screen shows garbled and fix it.
Investigating the garbled intro screen
Lets start off by investigating why the intro screen looks garbled.There is some random alpha numeric characters on the screen, suggesting that a custom character set is defined. We know for a fact that we haven't implemented custom characters yet in our emulator.
Let us do a quick refresher on how custom character sets work on the C64.
The VIC-II chip gives you the capability to specify different base locations in memory for the screen character memory and for the character set images. You configure the base addresses via location D018 and DD00. Location DD00 provides you with most significant two bits base address.
Something interesting about the character base addresses is that if it falls within the region 1000h-1fffh or 9000-9fffh in memory, then the VIC-II will read the character info from the character ROM. For all other regions the VIC-II will retrieve the info from RAM.
So, in a nutshell, if you set the character base address, avoiding above mentioned regions, you can define your own character set. Ok, it goes without saying that you actually need to write your character data at the specified base address.
Back to our problem. Lets do some memory inspection to determine the character base address. The inspection gives us the following:
- Memory location D018: 14
- Memory location DD00: 17
For another clue, lets stop the emulator and do some single stepping. Maybe we can see something interesting.
And, indeed there is something interesting:
5876 LDA $d012 5879 CMP #$d2 587b BNE $5876
The emulator is stuck in this loop. Keep checking till rasterline d2 arrives.
Currently the VIC registers just map to plain RAM registers in our emulator. So reading D012 will keep returning the same result.
It is time we hook up the VIC-II registers to the Video class so we can get some sensitible values when a program reads them.
From our discussion so far, we have a number of things to do in this bog. Lets quickly create a short list:
- Make Video class responsible for working with VIC-II register memory accesses
- Change memory class to delegate read/write requests for VICII registers to the Video class
- Assign a similar responsibility for the Video class regarding the color memory
- Add a method to the memory class that the video class can call for memory access. This method should closely model the VIC-II memory model. This method should have the following properties:
- Accept a 14 address
- Create an effective memory address via location DD00
- If the effective address is within the range 1000-1fff or 9000-9fff return data to the video class from the character ROM
- If the effective address is not within above mentioned region return requested contents from the main memory.
Tasking Video class with VICII register access
We have decided that VIC II register a should be the responsibility of the video class. There are 46 VIC registers, so lets start with defining a local array within the video class containing 46 elements, with accompanied getters and setters:function video(mycanvas, mem, cpu) { ... var registers = new Uint8Array(0x2e); ... this.writeReg = function (number, value) { registers[number] = value; } this.readReg = function (number) { return registers[number]; } ... }
Lets do the same exercise for the colorRAM:
function video(mycanvas, mem, cpu) { ... var colorRAM = new Uint8Array(1000); ... this.writeColorRAM = function(number, value) { colorRAM[number] = value; } this.readColorRAM = function(number) { return colorRAM[number]; }... }
We need to add some functionality when reading D012 to retrieve current raster line number:
this.readReg = function (number) { if (number == 0x11) { var bit8 = (cycleline & 0x100) >> 1; var temp = (registers[number] & 0x7f) | bit8; return temp; } else if (number == 0x12) { return (cycleline & 0xff); } else { return registers[number]; } }
You will notice that I have also included an entry for location D011. D011 contains the ninth bit of the raster counter.
We now need to change the Video class so that its internal operations also make use of the internal registers:
function video(mycanvas, mem, cpu) { ... function displayEnabled() { return ((registers[0x11] & 0x10) != 0) } ... function drawCharline() { var bitmapMode = ((registers[0x11] & 0x20) != 0) ? 1 : 0; var multicolorMode = ((registers[0x16] & 0x10) != 0) ? 1 : 0; var screenMode = (multicolorMode << 1) | bitmapMode; switch (screenMode) { //text mode, normal case 0: drawTextModeNormal(charPosInMem + cycleInLine - 5); break; //bitmap mode, normal case 1: break; //text mode, multi color case 2: break; //bitmap mode, multi color case 3: drawBitmapModeMultiColor(charPosInMem + cycleInLine - 5); break; } } function drawTextModeNormal(charPos) { var screenCode = localMem.readMem(1024 + charPos); var currentLine = localMem.readCharRom((screenCode << 3) + ((cycleline - 42) & 7)); var textColor = colorRAM[charPos] & 0xf; var backgroundColor = registers[0x21] & 0xf; var currentCol = 0; for (currentCol = 0; currentCol < 8; currentCol++) { var pixelSet = (currentLine & 0x80) == 0x80; if (pixelSet) { imgData.data[posInCanvas + 0] = colors[textColor][0]; imgData.data[posInCanvas + 1] = colors[textColor][1]; imgData.data[posInCanvas + 2] = colors[textColor][2]; imgData.data[posInCanvas + 3] = 255; } else { imgData.data[posInCanvas + 0] = colors[backgroundColor][0]; imgData.data[posInCanvas + 1] = colors[backgroundColor][1]; imgData.data[posInCanvas + 2] = colors[backgroundColor][2]; imgData.data[posInCanvas + 3] = 255; } currentLine = currentLine << 1; posInCanvas = posInCanvas + 4; } } function drawBitmapModeMultiColor(charPos) { var currentLine = localMem.readMem(0xe000+(charPos << 3) + ((cycleline - 42) & 7)); var textColor = colorRAM[charPos]; var backgroundColor = registers[0x21]; var color1 = (localMem.readMem(49152 + charPos) & 0xf0) >> 4; var color2 = localMem.readMem(49152 + charPos) & 0xf; var color3 = colorRAM[charPos] & 0xf; var colorArray = [backgroundColor, color1, color2, color3]; var pixPair = 0; for (pixPair = 0; pixPair < 4; pixPair++) { var colorValue = (currentLine >> 6) & 3; imgData.data[posInCanvas + 0] = colors[colorArray[colorValue]][0]; imgData.data[posInCanvas + 1] = colors[colorArray[colorValue]][1]; imgData.data[posInCanvas + 2] = colors[colorArray[colorValue]][2]; imgData.data[posInCanvas + 3] = 255; imgData.data[posInCanvas + 4] = colors[colorArray[colorValue]][0]; imgData.data[posInCanvas + 5] = colors[colorArray[colorValue]][1]; imgData.data[posInCanvas + 6] = colors[colorArray[colorValue]][2]; imgData.data[posInCanvas + 7] = 255; currentLine = currentLine << 2; posInCanvas = posInCanvas + 8; } } function fillBorderColor() { var borderColor = registers[0x20] & 0xf; var i; for (i = 0; i < 8; i++ ) { imgData.data[posInCanvas + 0] = colors[borderColor][0]; imgData.data[posInCanvas + 1] = colors[borderColor][1]; imgData.data[posInCanvas + 2] = colors[borderColor][2]; imgData.data[posInCanvas + 3] = 255; posInCanvas = posInCanvas + 4; } } ... }
We know need to modify the Memory class to use the Video class for VIC register accesses. It would probably make sense to add another statement to the nested if statement of readMem and writeMem. But, we already have IO region related entry for CIA access. I think it would look less clunky if we collate all IO region access into a mthod of its own.
With this in mind, our readMem and writeMem method will look as follows:
this.readMem = function (address) { if ((address >= 0xa000) & (address <=0xbfff)) return basicRom[address & 0x1fff]; else if ((address >= 0xe000) & (address <=0xffff) & kernelEnabled()) return kernalRom[address & 0x1fff]; else if ((address >= 0xd000) & (address <= 0xdfff) & IOEnabled()) { return IORead(address); } else if (address == 1) { var temp = mainMem[address] & 239; if (!playPressed) temp = temp | 16; return temp; } return mainMem[address]; } this.writeMem = function (address, byteval) { if ((address >= 0xd000) & (address <= 0xdfff) & IOEnabled()) { IOWrite(address, byteval); return; } else if (address == 1) { var temp = byteval & 32; temp = temp >> 5; tape.setMotorOn(temp); } mainMem[address] = byteval; }
The IORead and IOWrite methods look as follows:
function IORead(address) { if ((address >= 0xdc00) & (address <= 0xdcff)) { return ciaRead(address); } else if ((address >= 0xd000) & (address <= 0xd02e)) { return myVideo.readReg(address - 0xd000); } else if ((address >= 0xd800) & (address <= 0xdbe8)) { return myVideo.readColorRAM (address - 0xd800); } else { return IOUnclaimed[address - 0xd000]; } } function IOWrite(address, value) { if ((address >= 0xdc00) & (address <= 0xdcff)) { return ciaWrite(address, value); } else if ((address >= 0xd000) & (address <= 0xd02e)) { return myVideo.writeReg(address - 0xd000, value); } else if ((address >= 0xd800) & (address <= 0xdbe8)) { return myVideo.writeColorRAM (address - 0xd800, value); } else { IOUnclaimed[address - 0xd000] = value; return; } }
You will see in this code the use of an array called IOUnclaimed. This is for IO devices that we haven't implemented yet like CIA2 and SID. This just to encapsulate the block switch in/out functionality for the IO region. For completeness, let me show the declaration of the IOUnclaimed array:
.. var IOUnclaimed = new Uint8Array(4096); ...
The Memory class is now dependant on the Video class, so we need to add a setter in the memory class:
this.setVideo = function(video) { myVideo = video; }
This setter needs to be called by the index page during initialisation just after the a video instance is created:
... var myvideo = new video(document.getElementById("screen"), mymem, mycpu); mymem.setVideo(myvideo); ...
VIC II memory accesses
We now need to add a method to the memory class that the video class can call for memory access. This method will comply to the VIC-II memory model.Here is the code for that method:
this.vicRead = function(address) { var topBits = IOUnclaimed[0xd00] & 3; topBits = 3 - topBits; var effectiveAddress = (topBits << 14) | address; if ((effectiveAddress >= 0x9000) & (effectiveAddress < 0xa000)) { effectiveAddress = effectiveAddress & 0xfff; return charRom[effectiveAddress]; } else if ((effectiveAddress >= 0x1000) & (effectiveAddress < 0x2000)) { effectiveAddress = effectiveAddress & 0xfff; return charRom[effectiveAddress]; } else { return mainMem[effectiveAddress]; } }
This method gets bits 14 and 15 of the effective address from location DD00. It also reads the requested from character ROM if the effective address is within the range 1000-1fff or 9000-9fff.
We now need to modify the video class to use the method vicRead:
function drawTextModeNormal(charPos) { var baseCharAdd = (registers[0x18] >> 1) & 7; baseCharAdd = baseCharAdd << 11; var baseScreenAdd = (registers[0x18] >> 4) & 0xf; baseScreenAdd = baseScreenAdd << 10; var screenCode = localMem.vicRead(baseScreenAdd + charPos); var currentLine = localMem.vicRead(baseCharAdd + (screenCode << 3) + ((cycleline - 42) & 7)); var textColor = colorRAM[charPos] & 0xf; var backgroundColor = registers[0x21] & 0xf; var currentCol = 0; for (currentCol = 0; currentCol < 8; currentCol++) { var pixelSet = (currentLine & 0x80) == 0x80; if (pixelSet) { imgData.data[posInCanvas + 0] = colors[textColor][0]; imgData.data[posInCanvas + 1] = colors[textColor][1]; imgData.data[posInCanvas + 2] = colors[textColor][2]; imgData.data[posInCanvas + 3] = 255; } else { imgData.data[posInCanvas + 0] = colors[backgroundColor][0]; imgData.data[posInCanvas + 1] = colors[backgroundColor][1]; imgData.data[posInCanvas + 2] = colors[backgroundColor][2]; imgData.data[posInCanvas + 3] = 255; } currentLine = currentLine << 1; posInCanvas = posInCanvas + 4; } } function drawBitmapModeMultiColor(charPos) { var baseCharAdd = (registers[0x18] >> 1) & 7; baseCharAdd = baseCharAdd << 11; var baseScreenAdd = (registers[0x18] >> 4) & 0xf; baseScreenAdd = baseScreenAdd << 10; var currentLine = localMem.vicRead(baseCharAdd+(charPos << 3) + ((cycleline - 42) & 7)); var textColor = colorRAM[charPos]; var backgroundColor = registers[0x21]; var color1 = (localMem.vicRead(baseScreenAdd + charPos) & 0xf0) >> 4; var color2 = localMem.vicRead(baseScreenAdd + charPos) & 0xf; var color3 = colorRAM[charPos] & 0xf; var colorArray = [backgroundColor, color1, color2, color3]; var pixPair = 0; for (pixPair = 0; pixPair < 4; pixPair++) { var colorValue = (currentLine >> 6) & 3; imgData.data[posInCanvas + 0] = colors[colorArray[colorValue]][0]; imgData.data[posInCanvas + 1] = colors[colorArray[colorValue]][1]; imgData.data[posInCanvas + 2] = colors[colorArray[colorValue]][2]; imgData.data[posInCanvas + 3] = 255; imgData.data[posInCanvas + 4] = colors[colorArray[colorValue]][0]; imgData.data[posInCanvas + 5] = colors[colorArray[colorValue]][1]; imgData.data[posInCanvas + 6] = colors[colorArray[colorValue]][2]; imgData.data[posInCanvas + 7] = 255; currentLine = currentLine << 2; posInCanvas = posInCanvas + 8; } }
A Test Run
With all these changes, lets do a test run.The emulator didn't start so well:
Some closer investigation revealed that the problem is caused by the 6510 CPU register at memory location 1.
At startup I assign a value of 3. This is sufficient to ensure that your system boots, but it disables the IO Region. This has the effect that the IO register at location DD00 misses the write from the CPU, so the Video class gets its data from the wrong bank.
We obviously got away with this previously because we haven't implemented complete bank switching of the IO region. All IO writes/reads went straight to the main memory.
A save value to write to memory location 1 at startup is ffh. So, lets adjust this value and restart the emulator.
This time the world is back to normal. So, lets start loading Dan Dare and see how far our emulator can get this time.
This time around the intro screen is just a blank screen.
Some investigation revealed that something switched to multicolor text mode. This is puzzling, but lets anyway implement multi color text mode in the video class.
First we create a method within the Video class for the functionality:
function drawTextModeMultiColor(charPos) { var baseCharAdd = (registers[0x18] >> 1) & 7; baseCharAdd = baseCharAdd << 11; var baseScreenAdd = (registers[0x18] >> 4) & 0xf; baseScreenAdd = baseScreenAdd << 10; var screenCode = localMem.vicRead(baseScreenAdd + charPos); var currentLine = localMem.vicRead(baseCharAdd + (screenCode << 3) + ((cycleline - 42) & 7)); var textColor = colorRAM[charPos] & 0xf; if ((textColor & 8) == 0) return drawTextModeNormal(charPos); var backgroundColor = registers[0x21]; var color1 = registers[0x22]; var color2 = registers[0x23]; var color3 = textColor; var colorArray = [backgroundColor, color1, color2, color3]; var pixPair = 0; for (pixPair = 0; pixPair < 4; pixPair++) { var colorValue = (currentLine >> 6) & 3; imgData.data[posInCanvas + 0] = colors[colorArray[colorValue]][0]; imgData.data[posInCanvas + 1] = colors[colorArray[colorValue]][1]; imgData.data[posInCanvas + 2] = colors[colorArray[colorValue]][2]; imgData.data[posInCanvas + 3] = 255; imgData.data[posInCanvas + 4] = colors[colorArray[colorValue]][0]; imgData.data[posInCanvas + 5] = colors[colorArray[colorValue]][1]; imgData.data[posInCanvas + 6] = colors[colorArray[colorValue]][2]; imgData.data[posInCanvas + 7] = 255; currentLine = currentLine << 2; posInCanvas = posInCanvas + 8; } }
Then we add an item to the color mode switch selector:
function drawCharline() { var bitmapMode = ((registers[0x11] & 0x20) != 0) ? 1 : 0; var multicolorMode = ((registers[0x16] & 0x10) != 0) ? 1 : 0; var screenMode = (multicolorMode << 1) | bitmapMode; switch (screenMode) { //text mode, normal case 0: drawTextModeNormal(charPosInMem + cycleInLine - 5); break; //bitmap mode, normal case 1: break; //text mode, multi color case 2: drawTextModeMultiColor(charPosInMem + cycleInLine - 5); break; //bitmap mode, multi color case 3: drawBitmapModeMultiColor(charPosInMem + cycleInLine - 5); break; }
Lets restart our emulator once again with these changes and see how far our emulator can get.
This time something very interesting happened. The intro screen flash for a split of a second and then we are presented with the following screen:
We can some how make out that this is the game play screen, but it is also garbled. This is however an issue for the next blog.
We are more interested at this point in time, why the intro screen is skipped. It is as if something triggered the fire button.
To validate this theory on the fire button we need to know how joysticks interface on the Commodore 64. We will cover this in the next section.
Joystick Specifics
The snippet of a schematic shows how the two joystick ports is connected on the C64:The two joystick ports is named as Control Port 1 and Control Port 2 on the schematic. These two ports are connected to port A and port B respectively of the CIA chip number 1.
The two joystick ports therefore share the same lines as the keyboard.
Now a bit of the anatomy of a C64 joystick. For each of the following directions there is an associated switch in the joystick: North, South, West, East. So, if for instance you move the joystick in a northerly direction, the north switch will close. Likewise, for instance if you move the joystick in an eastern direction, the East switch will close.
What happens if you move the joystick in an diagonal direction, like North-East? Then two switches will close at once. For the North-East direction both the North switch and the East switch will close.
One fact worth mentioning is that when a joystick switch closely, a logic zero will be registered on the associated line on the CIA. Every switch that is not closed will register as a logic one.
This is starting to give us a clue on to why a firebutton is initiated shortly after the Dan Dare intro screen is shown.
When we read CIA#1 port A in our emulator a zero will be returned, because we never have initialised this location. In the joystick world a zero means that its direction switches are closed!
So, we should actually add a check that when CIA#1 port A is read we should return ffh. Lets implement this right away in the ciaRead method:
function ciaRead(address) { if (address == 0xdc00) { return 0xff; } else if (address == 0xdc01) { return keyboardInstance.getColumnByte(mainMem[0xdc00]); } else if (address == 0xdc04) { return mytimerA.getTimerLow(); } else if (address == 0xdc05) { return mytimerA.getTimerHigh(); } else if (address == 0xdc06) { return mytimerB.getTimerLow(); } else if (address == 0xdc07) { return mytimerB.getTimerHigh(); } else if (address == 0xdc0d) { return myinterruptController.getInterrupts(); } else if (address == 0xdc0e) { return mytimerA.getControlRegister(); } else if (address == 0xdc0f) { return mytimerB.getControlRegister(); } else { return mainMem[address]; } }
In the next blog we will worry about properly emulating a joystick.
Lets see how the emulator behaves with this change.
At last! A proper intro screen:
In Summary
In this blog we managed to fix the garbled screen.This investigation evolved to a refactoring exercise whereby we moved the responsibility of VICII register access to the video class. We also added
a method to the Memory class that the Video class can use for memory class.
We also implemented another screen mode: multi color text model.
Finally, we had to fix CIA#1 emulation so that it correctly emulate a joystick with all its switches open.
In the next blog we will work on properly implementing the joystick.
Till next time!
No comments:
Post a Comment