Foreword
In the previous blog we finished off with adding tape emulation to our emulator.In this blog we will try and see if we could emulate the flashing borders of the loader for game Dan Dare. This will necessitate us to jack up our video class by adding colours to the video output.
The flashing border effect
So, how does the tape loader achieve the flashing border effect?The tape loader achieves the flashing border effect by constantly changing the border colour while the VIC II video chip draws the frame.
For our emulator to simulate this effect successfully, this would mean that we need to implement some form of scan line rendering.
Currently our emulator doesn't do scan line-based rendering, but frame based rendering. This means, we execute the amount of instructions that would fit into a frame display period, which is 20000 CPU cycles.
So, let us start by doing a bit of research on the relationship of CPU cycles to screen pixels.
There is a very nice picture on the following web site explaining very nicely the relationship:
http://dustlayer.com/vic-ii/2013/4/25/vic-ii-for-beginners-beyond-the-screen-rasters-cycle
First of all this picture tells us that the CPU completes 63 clock cycles per scan line. Also, in one CPU clock cycle, 8 pixels gets drawn to the screen.
It is also interesting to note that for each line of 63 cycles, there is 13 cycles for which there is no pixels drawn. These cycles are during the horizontal blanking period.
Although we don't need to draw anything for these 13 cycles per line, it is still important that we account for these cycles to get our timing right.
As you get a horizontal blanking period, you also get a vertical blanking period. The vertical blanking period is towards the end of the frame, wrapping to the beginning of the next frame. This period amounts to about 28 lines that amounts to nothing drawn unto the screen. These lines we also need to account for to get our timing write.
Implementing scan line rendering
So, lets implement scan line rendering into our emulator.First, we need to get hold of RGB values for the 16 colours on the C64.
If you do a Internet search for these RGB values, you will get different values all over the show. This is probably as expected, since we all have different memories of these colours on these colours on our television sets at that time when the C64 was popular.
I am settling for the colour tablet from the following website:
https://www.c64-wiki.com/index.php/Color
With all this information, our video class looks as follows:
function video(mycanvas, mem, cpu) { var localMem = mem; var ctx = mycanvas.getContext("2d"); var mycpu = cpu; var cpuCycles = 0; var cycleInLine = 0; var cycleline = 0; var charPosInMem = 0; var posInCanvas = 0; var imgData = ctx.createImageData(400, 284); const colors = [[0, 0, 0], [255, 255, 255], [136, 0, 0], [170, 255, 238], [204, 68, 204], [0, 204, 85], [0, 0, 170], [238, 238, 119], [221, 136, 85], [102, 68, 0], [255, 119, 119], [51, 51, 51], [119, 119, 119], [170, 255, 102], [0, 136, 255], [187, 187, 187]]; this.getCurrentLine = function() { return cycleline; } this.processpixels = function() { var numBytes = mycpu.getCycleCount() - cpuCycles; cpuCycles = mycpu.getCycleCount(); var i; for (i = 0; i < numBytes; i++) { if (isVisibleArea()) { if (isPixelArea() & displayEnabled()) { drawCharline(); } else { fillBorderColor(); } } cycleInLine++; if (cycleInLine > 63) { cycleInLine = 0; cycleline++; updateCharPos(); } if (cycleline > 311) { cycleline = 0; ctx.putImageData(imgData,0,0); posInCanvas = 0; charPosInMem = 0; //imgData = ctx.createImageData(400, 284); return true; } } return false; } function displayEnabled() { return ((localMem.readMem(0xd011) & 0x10) != 0) } function updateCharPos() { if ( !((cycleline > 41) & (cycleline < (42 + 200))) ) return; var lineInScreen = cycleline - 42; if (lineInScreen == 0) { charPosInMem = 0; return; } if ((lineInScreen & 7) == 0) { charPosInMem = charPosInMem + 40; } } function drawCharline() { var screenCode = localMem.readMem(1024 + charPosInMem + cycleInLine - 5); var currentLine = localMem.readCharRom((screenCode << 3) + ((cycleline - 42) & 7)); var textColor = localMem.readMem(0xd800 + charPosInMem + cycleInLine - 5); var backgroundColor = localMem.readMem(0xd021); 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 fillBorderColor() { var borderColor = localMem.readMem(0xd020) & 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; } } function isVisibleArea() { return (cycleInLine < 50) & (cycleline < 284); } function isPixelArea() { var visibleColumn = (cycleInLine > 4) & (cycleInLine < (5+40)); var visibleRow = (cycleline > 41) & (cycleline < (42 + 200)); return (visibleColumn & visibleRow); } }
Let us quickly pause and explain this code. The class start with the declaration of a two dimensional array storing the RGB values for our colour tablet.
Obviously, the updateCanvas method has became redundant. We now make use use of a new method called processpixels.
The processpixels method needs to be invoked each time a 6502 instruction was executed. processpixels starts off by determining how many cycles has passed since executing the previous instruction. For each cycle an 8 pixel lines segment gets drawn. During the looping process we also keep track of the following:
- Whether the pixel segment is a segment in the border
- Whether the pixel segment is a segment in the character area.
- Whether the pixel segment is a segment in the vertical/horizontal blanking area.
function runBatch() { if (!running) return; //myvideo.updateCanvas(); //var targetCycleCount = mycpu.getCycleCount() + 20000; while (true) { mycpu.step(); myAlarmManager.processAlarms(); //if (mycpu.getCycleCount() > 6000000) // mymem.setSimulateKeypress(); //var blankingPeriodLow = targetCycleCount - 100; if (myvideo.getCurrentLine() > 284) { mymem.writeMem(0xD012, 0); } else { mymem.writeMem(0xD012, 1); } if (mycpu.getPc() == breakpoint) { stopEm(); return; } var framefinished = myvideo.processpixels(); if (framefinished) return; } //mycpu.setInterrupt(); }
You will notice that the main loop within the runBatch method is now an endless loop, depending on the processpixels method to tell it when to exit.
One fundamental change is also the dimensions of the canvas we are working with. The new dimensions is now 400x284, since we need to cater for the border. This dimension needs to be applied to the video class as well as our index page.
A Test Run
Let us give our changes a test run:Cool! Everything starts to look very familiar now :-)
Let us now see how our emulator behaves when we load DAN DARE.
This time not so promising. We get up to the point where our emulator says loading/ready and then it just hangs.
It is time we get our fingers dirty and dig into the loader code for Dan Dare.
We need to inspect the header to determine at which memory location the loader code gets loaded into. We know that the header gets loaded into 33Ch, so the only thing we need to do is to hit the STOP button when we see FOUND... From that point we can just do some memory inspection to get the address.
The byte in 33c gives you the type of file. Type 3 corresponds to a non-relocatable file. The next two bytes is the start address and the two bytes following is the end address.
From the screenshot we can deduce that the start address is 2a7 and the end address is 304.
From this address range it is clear that some vector addresses will be overwritten.
The question is which vector address will be overwritten. To get this answer we need to load the rest of the loader and inspect the vectors. We can just resume execution of the emulator. But, before we resume, lets just add a breakpoint on address at fc93. This will halt the emulator just after the loader code is loaded into memory.
Inspecting the first couple of vectors yield the following:
300 = e38b 302 = 351 304 = a57c
We can clearly see that the vector at 302 is overwritten. This is the address of the basic idle loop. The address 351 is a memory address within the cassette buffer. So, it appear that the loader use part of the bytes in the header to store some machine code.
Lets do some disassembly of the code at 351:
* = 0351 0351 20 A7 02 JSR $02A7 0354 A5 FD LDA $FD 0356 05 FE ORA $FE 0358 F0 F7 BEQ $0351 035A 20 60 03 JSR $0360 035D 4C 51 03 JMP $0351 0360 6C FD 00 JMP ($00FD) 0363 AD 0D DC LDA $DC0D 0366 8E 0F DC STX $DC0F 0369 4A LSR A 036A 4A LSR A 036B 26 C0 ROL $C0 036D A5 C0 LDA $C0 036F 88 DEY 0370 F0 00 BEQ $0372 0372 AD 20 D0 LDA $D020 0375 49 05 EOR #$05 0377 8D 20 D0 STA $D020 037A 40 RTI 037B C9 AA CMP #$AA 037D D0 6E BNE $03ED 037F C6 C1 DEC $C1 0381 D0 05 BNE $0388 0383 A9 19 LDA #$19 0385 8D 71 03 STA $0371 0388 A0 02 LDY #$02 038A 40 RTI 038B C8 INY 038C 4A LSR A 038D B0 5E BCS $03ED 038F C9 50 CMP #$50 0391 D0 F5 BNE $0388 0393 A9 25 LDA #$25 0395 D0 44 BNE $03DB 0397 85 FB STA $FB 0399 EE 98 03 INC $0398 039C AD 98 03 LDA $0398 039F C9 FF CMP #$FF 03A1 D0 3B BNE $03DE 03A3 A5 F7 LDA $F7 03A5 CD FD 02 CMP $02FD 03A8 D0 43 BNE $03ED 03AA AD 11 D0 LDA $D011 03AD 29 EF AND #$EF 03AF 05 F8 ORA $F8 03B1 8D 11 D0 STA $D011 03B4 A9 46 LDA #$46 03B6 D0 23 BNE $03DB 03B8 84 01 STY $01 03BA 91 F9 STA ($F9),Y 03BC A0 05 LDY #$05 03BE 84 01 STY $01 03C0 45 AF EOR $AF 03C2 85 AF STA $AF 03C4 E6 F9 INC $F9 03C6 D0 05 BNE $03CD 03C8 EE 20 D0 INC $D020 03CB E6 FA INC $FA 03CD A5 F9 LDA $F9 03CF C5 FB CMP $FB 03D1 D0 0B BNE $03DE 03D3 A5 FA LDA $FA 03D5 C5 FC CMP $FC 03D7 D0 05 BNE $03DE 03D9 A9 6F LDA #$6F 03DB 8D 71 03 STA $0371 03DE A0 08 LDY #$08 03E0 40 RTI 03E1 C6 AE DEC $AE 03E3 C5 AF CMP $AF 03E5 F0 F7 BEQ $03DE 03E7 EE 20 D0 INC $D020 03EA 4C E7 03 JMP $03E7 03ED A9 40 LDA #$40 03EF 85 C1 STA $C1 03F1 A9 09 LDA #$09 03F3 8D 71 03 STA $0371 03F6 C8 INY 03F7 40 RTI 03F8 00 BRK 03F9 00 BRK 03FA 00 BRK 03FB 00 BRK 03FC 00 BRK 03FD 00 BRK 03FE 00 BRK 03FF 00 BRK 0400 .END
The first instruction calls a subroutine at 2a7, so let us also do a disassembly at that location:
* = 02A7 02A7 A9 7F LDA #$7F 02A9 8D 0D DC STA $DC0D 02AC A9 5E LDA #$5E 02AE 8D 06 DC STA $DC06 02B1 A0 01 LDY #$01 02B3 8C 07 DC STY $DC07 02B6 88 DEY 02B7 8C 1A D0 STY $D01A 02BA 84 AE STY $AE 02BC 84 AF STY $AF 02BE A9 63 LDA #$63 02C0 8D FE FF STA $FFFE 02C3 A9 7A LDA #$7A 02C5 8D FA FF STA $FFFA 02C8 A9 03 LDA #$03 02CA 8D FF FF STA $FFFF 02CD 8D FB FF STA $FFFB 02D0 A9 7B LDA #$7B 02D2 8D 71 03 STA $0371 02D5 A9 F7 LDA #$F7 02D7 8D 98 03 STA $0398 02DA A2 19 LDX #$19 02DC A9 05 LDA #$05 02DE 85 01 STA $01 02E0 A9 90 LDA #$90 02E2 8D 0D DC STA $DC0D 02E5 24 AE BIT $AE 02E7 10 FC BPL $02E5 02E9 A9 7F LDA #$7F 02EB 8D 0D DC STA $DC0D 02EE A9 37 LDA #$37 02F0 85 01 STA $01 02F2 85 C0 STA $C0 02F4 A9 81 LDA #$81 02F6 8D 0D DC STA $DC0D 02F9 EE FD 02 INC $02FD 02FC 60 RTS 02FD 00 BRK 02FE 00 BRK 02FF 00 BRK 0300 8B ??? 0301 E3 ??? 0302 51 03 EOR ($03),Y 0304 7C ??? 0305 A5 1A LDA $1A 0307 A7 ??? 0308 E4 A7 CPX $A7 030A 86 AE STX $AE 030C 00 BRK 030D 00 BRK 030E 00 BRK 030F 00 BRK 0310 4C 48 B2 JMP $B248 0313 00 BRK 0314 2C F9 66 BIT $66F9 0317 FE 47 FE INC $FE47,X 031A 4A LSR A 031B F3 ??? 031C 91 F2 STA ($F2),Y 031E 0E F2 50 ASL $50F2 0321 F2 ??? 0322 33 ??? 0323 F3 ??? 0324 57 ??? 0325 F1 CA SBC ($CA),Y 0327 F1 ED SBC ($ED),Y 0329 F6 3E INC $3E,X 032B F1 2F SBC ($2F),Y 032D F3 ??? 032E 66 FE ROR $FE 0330 A5 F4 LDA $F4 0332 ED F5 00 SBC $00F5 0335 00 BRK 0336 00 BRK 0337 00 BRK 0338 00 BRK 0339 00 BRK 033A 00 BRK 033B 00 BRK 033C 03 ??? 033D A7 ??? 033E 02 ??? 033F 04 ??? 0340 .END
Aha! This piece of code change the IRQ address at memory location FFFE an FFFF and switches the kernel ROM out of view via memory location 1 so that the CPU can see the new IRQ address.
We haven't implemented the in/out switching of ROMS in our emulator. SO, lets implement functionality for switching in/out the KERNEL ROM.
We add the following method to our memory class:
function kernelEnabled() { temp = mainMem[1] & 3; return (temp >= 2); }
We then use this method within readMem:
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 >= 0xdc00) & (address <= 0xdcff)) { return ciaRead(address); } else if (address == 1) { var temp = mainMem[address] & 239; if (!playPressed) temp = temp | 16; return temp; } return mainMem[address]; }
It is important to set memory location to 3 when you create the memory class, else the system will not boot:
function memory(allDownloadedCallback, keyboard, timerA, timerB, interruptController,tape) { var mytimerA = timerA; var mytimerB = timerB; var myinterruptController = interruptController; var mytape = tape; var mainMem = new Uint8Array(65536); mainMem[1] = 3; var basicRom = new Uint8Array(8192); var kernalRom = new Uint8Array(8192); ...
}
Let us try again. This time there was a faint piece of hope... a second of flashing borders, then it stopped.
Probably the first thing to do is to determine if the Tape class continues to fire interrupts at his point. Another thing to check is also whether the tape class is at the correct position on tape at this point.
To summarise my findings on this issue. I found that the tape class indeed continued to fire interrupts when the flashing borders stopped. What I did picked up was that the Tape interrupt always fired before TIMER B expired. This meant that only a stream of zeros was was identified, no ones.
After some digging, I found that the culprit was the setTimerLow method in the timer class:
this.setTimerLow = function(low) { timerHigh = low; }
The setter was setting the timerHigh value instead of timerLow!
This time we have better luck. Here is a set of screen shots:
In the last screenshot, the emulator was actually supposed to switch the screen to a high resolution multi-color mode for the splash screen. We will make the high resolution mode the task of our next blog.
In Summary
In this blog we jacked up our Video class so it can output colors. When then attempted to emulate the flashing flashing borders of the Dan Dare tape loader.We encountered a couple of bugs in our emulators, which we have resolved.
In the next blog we will implement high resolution mode and see if we can display the splash screen of Dan Dare tape loader.
Till next time!
Loving this! You rock.
ReplyDelete