Foreword
In the previous blog we fixed the garbled screen when we started the game Dan Dare within our emulator Dan Dare within our emulator.
Despite our progress, we ended off the previous blog with Dan Dare and the Treens still invisible.
In this Blog we will bring Dan Dare and the Treens to live. As a matter of fact, Dan Dare and the Treens are sprites, which we haven't implemented yet in our emulator.
So, in this Blog we will implement sprites.
This Blog will conclude my series on Writing a JavaScript emulator scratch.
Introduction to C64 Sprites
A Sprite is a small hi resolution image that you can move around the screen with minimum effort.
On the Commodore 64 the resolution of a Sprite is 24x21 and you can display up to eight sprites simultaneously.
The complexity of Sprites comes in with the fact that they can overlap each other and potentially other graphic objects like text characters. When developing a game involving Sprites, one should specify for the graphics chip which graphic objects are the objects that should be hidden in a case of overlapping.
The VIC II chip uses priorities to allow the developer to specify which objects should be concealed in the case of overlapping. Before we continue, lets us define the term priority within the context of the VIC II chip by means of an example: If Sprite A obscures the view of Sprite B, Sprite A is said to have a higher priority than Sprite B.
The first mechanism for specifying Sprite priority within the VIC-II is via the sprite numbers it self. The lower the sprite number, the higher its priority. For instance, Sprite number 0 will always have the highest priority, therefore it is not possible for another sprite to obscure sprite 0. Likewise, for instance, sprite number two will always obscure sprite number four.
How do specify whether a sprite should appear in front or behind text? You specify this via the sprite collision register at location D01B. Each bit in this register corresponds to a specific sprite. If the bit for a specific sprite is set to a one, this sprite will appear behind text. If the bit is set to a zero, it will appear in front of text.
With all this theory covered, one might wonder: How do you implement sprites within our emulator? This seems like a lot of potential record keeping to determine when to draw a particular sprite in order to ensure that the correct sprite gets concealed.
With HTML5 at our disposal, we should let canvasses do the work for us as much as possible, in order to limit the amount of record keeping we need to do to conceal the correct sprite.
A nice thing about HTML 5 canvases is that you can stack them on top of each other. What is even more attractive about an HTML 5 canvas is that each pixel on it has an alpha channel which you can use to specify transparency.
With above mentioned two properties of a HTML5 canvas, I can already see how canvasses can aid us in dealing with overlapping sprites.
My idea is basically to define the following canvases and stack them on top of each other:
- Background canvas
- Canvas for sprites having a priority for appearing behind text
- Foreground canvas. You will use this canvas to draw text
- Canvas for sprites having a priority for appearing in front of text
To deal with priorities of overlapping sprites, we should be careful about the drawing order of sprites. When we draw sprites, we need to draw them in sequence starting with the lowest priority sprite (e.g. sprite number 7) and working our way up to the highest priority sprite (e.g. sprite number 0).
Let us try to visualise this stacked canvases. I will take the following screen shot from the game Dan Dare as an example:
In this scene, Dan Dare is under water with only a tip of Reed sticking out of the water as seen on the top left of the water.
I will now show you a sequence of screen shots, showing how each individual canvas in our emulator (as in the state by the end of this blog) would look like if you unstack them.
First the background layer:
Next, the canvas for sprites appearing behind text:
Next , the foreground layer:
By far, the most most messy layer is the background layer. This is a side-effect of my attempt to minimise writes to canvasses for performance reasons. Basically I only write to a particular pixel on the background canvas if it is not obscured by the foreground canvas. So, the "dirt" is actually ghost images of a few moments back.
Just one final remark about the messiness about the background canvas. This is quit evident that the game makes extensive use of inverse video. This actually made me click, in the debugging session of my previous blog, why 81h was used for the screen code of an A instead of one: It is inverse video! With inverse video, bit 7 is always set in the screen code.
Implementing Stacked canvases in HTML 5
Let us implement the stacked canvasses into our index page, as mentioned in the previous section:... <body onkeydown="mykeyboard.onkeydown(event)" onkeyup="mykeyboard.onkeyup(event)"> <h1>6502 Emulator From Scratch</h1> <p>This is JavaScript Test</p> <div style="position:relative; width:400px; height:284px"> <canvas id="screenBackground" width="400" height="284" style="z-index: 1; position:absolute; left: 0px; top: 0px"> </canvas> <canvas id="spriteBackground" width="400" height="284" style="z-index: 2; position:absolute; left: 0px; top: 0px"> </canvas> <canvas id="screenForeground" width="400" height="284" style="z-index: 3; position:absolute; left: 0px; top: 0px"> </canvas> <canvas id="spriteForefround" width="400" height="284" style="z-index: 4; position:absolute; left: 0px; top: 0px"> </canvas> </div><br/> <input type="file" id="file" name="myfile"/> <button onclick="myTape.attachTape(document.getElementById('file').files[0])">Attach</button> <br/> ...
The absolute positioning of the canvases at left and right position zero onto the div element ensure that all four canvasses are stacked onto each other.
The z-index style defined for each canvas ensures the canvasses are stacked in the correct order. The higher the z-index, the higher its visibility priority.
We should now modify our video class to make use of these new canvasses:
function video(backgroundCanvas, spriteBackgroundCanvas, foregroundCanvas, spriteforegroundCanvas, mem, cpu) { var localMem = mem; var ctxBackground = backgroundCanvas.getContext("2d"); var ctxSpriteBackground = spriteBackgroundCanvas.getContext("2d"); var ctxForeground = foregroundCanvas.getContext("2d"); var ctxSpriteForeground = spriteforegroundCanvas.getContext("2d"); ... var backgroundData = ctxBackground.createImageData(400, 284); var spriteBackgroundData = ctxSpriteBackground.createImageData(400, 284); var foregroundData = ctxForeground.createImageData(400, 284); var spriteForegroundData = ctxSpriteForeground.createImageData(400, 284); ... }
Obviously, we need to modify the video instantiation call on our index page as well:
... myInterruptController.setCpu(mycpu); mycpu.setInterruptController(myInterruptController); var myvideo = new video(document.getElementById("screenBackground"), document.getElementById("spriteBackground"), document.getElementById("screenForeground"), document.getElementById("spriteForefround"), mymem, mycpu); mymem.setVideo(myvideo); mycpu.setVideo(myvideo); ...
Next, we should modify existing methods writing to the screen to use the correct canvasses:
function drawTextModeNormal(charPos) { ... for (currentCol = 0; currentCol < 8; currentCol++) { var pixelSet = (currentLine & 0x80) == 0x80; if (pixelSet) { foregroundData.data[posInCanvas + 0] = colors[textColor][0]; foregroundData.data[posInCanvas + 1] = colors[textColor][1]; foregroundData.data[posInCanvas + 2] = colors[textColor][2]; foregroundData.data[posInCanvas + 3] = 255; } else { foregroundData.data[posInCanvas + 3] = 0; backgroundData.data[posInCanvas + 0] = colors[backgroundColor][0]; backgroundData.data[posInCanvas + 1] = colors[backgroundColor][1]; backgroundData.data[posInCanvas + 2] = colors[backgroundColor][2]; backgroundData.data[posInCanvas + 3] = 255; } currentLine = currentLine << 1; posInCanvas = posInCanvas + 4; } ... } function drawTextModeMultiColor(charPos) { ... for (pixPair = 0; pixPair < 4; pixPair++) { var colorValue = (currentLine >> 6) & 3; if (colorValue >= 2) { foregroundData.data[posInCanvas + 0] = colors[colorArray[colorValue]][0]; foregroundData.data[posInCanvas + 1] = colors[colorArray[colorValue]][1]; foregroundData.data[posInCanvas + 2] = colors[colorArray[colorValue]][2]; foregroundData.data[posInCanvas + 3] = 255; foregroundData.data[posInCanvas + 4] = colors[colorArray[colorValue]][0]; foregroundData.data[posInCanvas + 5] = colors[colorArray[colorValue]][1]; foregroundData.data[posInCanvas + 6] = colors[colorArray[colorValue]][2]; foregroundData.data[posInCanvas + 7] = 255; } else { backgroundData.data[posInCanvas + 0] = colors[colorArray[colorValue]][0]; backgroundData.data[posInCanvas + 1] = colors[colorArray[colorValue]][1]; backgroundData.data[posInCanvas + 2] = colors[colorArray[colorValue]][2]; backgroundData.data[posInCanvas + 3] = 255; foregroundData.data[posInCanvas + 3] = 0; backgroundData.data[posInCanvas + 4] = colors[colorArray[colorValue]][0]; backgroundData.data[posInCanvas + 5] = colors[colorArray[colorValue]][1]; backgroundData.data[posInCanvas + 6] = colors[colorArray[colorValue]][2]; backgroundData.data[posInCanvas + 7] = 255; foregroundData.data[posInCanvas + 7] = 0; } ... } } function fillBorderColor() { var borderColor = registers[0x20] & 0xf; var i; for (i = 0; i < 8; i++ ) { foregroundData.data[posInCanvas + 0] = colors[borderColor][0]; foregroundData.data[posInCanvas + 1] = colors[borderColor][1]; foregroundData.data[posInCanvas + 2] = colors[borderColor][2]; foregroundData.data[posInCanvas + 3] = 255; posInCanvas = posInCanvas + 4; } }
Implementing Sprite functionality
Time to implement the functionality for drawing the sprites within our emulator.We will make the sprite drawing functionality part of the main loop in processpixels:
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(); processSprites(); } else { fillBorderColor(); } } ... } ... }
The outline of the processSprites method looks as follows:
function processSprites() { var i = 0; var spriteBit = 0x80; for (i = 0; i < 8; i++) { var currentSprite = 7 - i; spriteBit = 0x80 >> i; if ((registers[0x15] & spriteBit) == 0) continue; } }
We mentioned in a previous section that we need to start drawing from sprite number 7 and work towards sprite zero. If one reads through the documentation of JavaScript you will soon see that counting down loops is not as effective as counting up loops. For this reason I implemented a count up loop and determine the currentSprite with 7 - i.
Just one final remark. You will notice the check against register 15 in every loop iteration. Register 15 indicates which sprites are enabled. So, if the particular sprite in question is disabled we skip the loop iteration with a continue.
Determining when to draw sprite data
Determining when to draw a sprite is probably the biggest challenge to implement with our sprite drawing.Within our video class there is two variables that can assist us in this process: cycleline and cycleInLine.
The first check we need to perform is if cycleline falls within Sprite Postion y and Sprite postion Y + 21. The addition of 21 is of course because a sprite is 21 lines high.
We would expect a similar check with cycleInLine. There is, however, a complexity with the fact that cycleInLine will provide you with a x-position that is a multiple of 8. The x-postion of a sprite can however be any where in between two integer multiples of eight. This scenario is illustrated by the following:
48 . . . . . . .56 . . . . . . .64 . . . . . . .72 . . . . . . .80 X - - - - o o o X o o o o o o o X o o o o o o o X o o o o - - - X X - - - - o o o X o o o o o o o X o o o o o o o X o o o o - - - X X - - - - o o o X o o o o o o o X o o o o o o o X o o o o - - - X
A typical cycleInLine will start at, for eaxmple at pixel column 48, and end at pixel column 55. I have marked all the start of each cycleinline with an X.
The writing of a sprite pixel I have indicated with an o. As you can see in this example the pixels of a sprite can indeed only partly fill a cycleInLine in many occasions.
A natural instinct would to check for each and every pixel and see if it forms part of a sprite. This can, however, potentially be a computational expensive exercise.
An alternative to the pixel by pixel approach would be to check if the start pixel of a cycleInLine falls within the line of a sprite and do the same check for the last pixel of a cycleInLine (aka start pixel + 7). With the result of these checks established we perform an action based on the following thruth table:
Start Pixel within sprite line | Start Pixel + 7 within sprite line | Required action |
---|---|---|
False | False | Nothing. Continue to next Sprite |
False | True | Within cycleInLine draw sprite data from (SpritePosX & 7) to StartPixel+7. Starting pixel within Sprite Color line source: Pixel number: zero. |
True | False | Within cycleInLine start to draw sprite data from pixel zero to (SpritePosX & 7) Starting pixel within Sprite Color line source: Pixel Number: Sprite pixel width - (SpritePosX & 7) |
True | True | Within cycleInLine start to draw sprite data from pixel zero to 7 Starting pixel within Sprite Color line source: Pixel Number: (xpos of cycleline - SpritePosX) |
In this table you will see reference to Sprite color line source. This is the current sprite line in question rendered into its final colors in the format rgba. More on this in the next section
Rendering a Sprite Line
Rendering a sprite line into its final colors is a very important function within our emulator. This process is complicated by two facts:- A sprite can be multicolored where two consecutive pixels is assigned the same color.
- The VIC II chip allows you to specify that a sprite is displayed in double its original width
So where do we start? First we need to get the actual data for the applicable sprite line from memory as a 24 bit number:
function getSpriteLineData (spriteNumber, spriteLine) { var spritePointerAdd = (registers[0x18] >> 4) & 0xf; spritePointerAdd = spritePointerAdd << 10; spritePointerAdd = spritePointerAdd + 0x3f8 + spriteNumber; spriteBaseAdd = localMem.vicRead(spritePointerAdd) << 6; posInSpriteData = spriteLine * 3 + spriteBaseAdd; spriteDataByte0 = localMem.vicRead(posInSpriteData + 0); spriteDataByte1 = localMem.vicRead(posInSpriteData + 1); spriteDataByte2 = localMem.vicRead(posInSpriteData + 2); return (spriteDataByte0 << 16) | (spriteDataByte1 << 8) | (spriteDataByte2 << 0); }
This method starts off to calculate the address of the pointer for the data for the sprite in question.
Each sprite pointer is a single byte located just after screen memory. So, the pointer address is calculated by first determining the start off screen and then adding 3f8h to it.
The sprite pointer itself is a 64 byte block number, so we need to multiply this value by 64 to get the actual address.
The rest of the method is pretty straight forward. Changing the sprite line number to a linear address by multiplying it by 3. Finally we do some bit shifting and or'ing to form a 24 bit number of the 3 bytes contained in the sprite line.
It is time now to write a method for rendering the sprite line into its final rgba colors. Considering that a sprite can be multi colored and potentially be expanded to double its original size, we will have four different methods for this functionality:
- Rendering the sprite colors for a monochrome sprite
- Rendering the sprite colors for a multi color sprite
- Rendering the sprite colors for a monochrome expanded sprite
- Rendering the sprite colors for a multi color expanded sprite.
... var spriteColorLine = new Uint8Array(48 * 4); ... function populateSpriteColorLine (spriteNumber, spriteLine) { var spriteData = getSpriteLineData(spriteNumber, spriteLine); var spriteColor = registers[0x27 + spriteNumber] & 0xf; var i = 0; posInColorLine = 0; for (i = 0; i < 24; i++) { var currentBit = (spriteData >> 23) & 1; if (currentBit == 1) { spriteColorLine [posInColorLine + 0] = colors[spriteColor][0]; spriteColorLine [posInColorLine + 1] = colors[spriteColor][1]; spriteColorLine [posInColorLine + 2] = colors[spriteColor][2]; spriteColorLine [posInColorLine + 3] = 255; } else { spriteColorLine [posInColorLine + 0] = 0; spriteColorLine [posInColorLine + 1] = 0; spriteColorLine [posInColorLine + 2] = 0; spriteColorLine [posInColorLine + 3] = 0; } posInColorLine = posInColorLine + 4; spriteData = (spriteData << 1) & 0xffffff; } } function populateSpriteMultiColorLine (spriteNumber, spriteLine) { var spriteData = getSpriteLineData(spriteNumber, spriteLine); var spriteColor = registers[0x27 + spriteNumber] & 0xf; var multicolorPalette = [-1, (registers[0x25] & 0xf), spriteColor, (registers[0x26] & 0xf)]; var i = 0; posInColorLine = 0; for (i = 0; i < 12; i++) { var currentBits = (spriteData >> 22) & 3; if (currentBits > 0) { spriteColorLine [posInColorLine + 0] = colors[multicolorPalette[currentBits]][0]; spriteColorLine [posInColorLine + 1] = colors[multicolorPalette[currentBits]][1]; spriteColorLine [posInColorLine + 2] = colors[multicolorPalette[currentBits]][2]; spriteColorLine [posInColorLine + 3] = 255; spriteColorLine [posInColorLine + 4] = colors[multicolorPalette[currentBits]][0]; spriteColorLine [posInColorLine + 5] = colors[multicolorPalette[currentBits]][1]; spriteColorLine [posInColorLine + 6] = colors[multicolorPalette[currentBits]][2]; spriteColorLine [posInColorLine + 7] = 255; } else { spriteColorLine [posInColorLine + 0] = 0; spriteColorLine [posInColorLine + 1] = 0; spriteColorLine [posInColorLine + 2] = 0; spriteColorLine [posInColorLine + 3] = 0; spriteColorLine [posInColorLine + 4] = 0; spriteColorLine [posInColorLine + 5] = 0; spriteColorLine [posInColorLine + 6] = 0; spriteColorLine [posInColorLine + 7] = 0; } posInColorLine = posInColorLine + 8; spriteData = (spriteData << 2) & 0xffffff; } } function populateSpriteColorLineExpanded (spriteNumber, spriteLine) { var spriteData = getSpriteLineData(spriteNumber, spriteLine); var spriteColor = registers[0x27 + spriteNumber] & 0xf; var i = 0; posInColorLine = 0; for (i = 0; i < 24; i++) { var currentBit = (spriteData >> 23) & 1; if (currentBit == 1) { spriteColorLine [posInColorLine + 0] = colors[spriteColor][0]; spriteColorLine [posInColorLine + 1] = colors[spriteColor][1]; spriteColorLine [posInColorLine + 2] = colors[spriteColor][2]; spriteColorLine [posInColorLine + 3] = 255; spriteColorLine [posInColorLine + 4] = colors[spriteColor][0]; spriteColorLine [posInColorLine + 5] = colors[spriteColor][1]; spriteColorLine [posInColorLine + 6] = colors[spriteColor][2]; spriteColorLine [posInColorLine + 7] = 255; } else { spriteColorLine [posInColorLine + 0] = 0; spriteColorLine [posInColorLine + 1] = 0; spriteColorLine [posInColorLine + 2] = 0; spriteColorLine [posInColorLine + 3] = 0; spriteColorLine [posInColorLine + 4] = 0; spriteColorLine [posInColorLine + 5] = 0; spriteColorLine [posInColorLine + 6] = 0; spriteColorLine [posInColorLine + 7] = 0; } posInColorLine = posInColorLine + 8; spriteData = (spriteData << 1) & 0xffffff; } } function populateSpriteMultiColorLineExpanded (spriteNumber, spriteLine) { var spriteData = getSpriteLineData(spriteNumber, spriteLine); var spriteColor = registers[0x27 + spriteNumber] & 0xf; var multicolorPalette = [-1, (registers[0x25] & 0xf), spriteColor, (registers[0x26] & 0xf)]; var i = 0; posInColorLine = 0; for (i = 0; i < 12; i++) { var currentBits = (spriteData >> 22) & 3; if (currentBits > 0) { spriteColorLine [posInColorLine + 0] = colors[multicolorPalette[currentBits]][0]; spriteColorLine [posInColorLine + 1] = colors[multicolorPalette[currentBits]][1]; spriteColorLine [posInColorLine + 2] = colors[multicolorPalette[currentBits]][2]; spriteColorLine [posInColorLine + 3] = 255; spriteColorLine [posInColorLine + 4] = colors[multicolorPalette[currentBits]][0]; spriteColorLine [posInColorLine + 5] = colors[multicolorPalette[currentBits]][1]; spriteColorLine [posInColorLine + 6] = colors[multicolorPalette[currentBits]][2]; spriteColorLine [posInColorLine + 7] = 255; spriteColorLine [posInColorLine + 8] = colors[multicolorPalette[currentBits]][0]; spriteColorLine [posInColorLine + 9] = colors[multicolorPalette[currentBits]][1]; spriteColorLine [posInColorLine + 10] = colors[multicolorPalette[currentBits]][2]; spriteColorLine [posInColorLine + 11] = 255; spriteColorLine [posInColorLine + 12] = colors[multicolorPalette[currentBits]][0]; spriteColorLine [posInColorLine + 13] = colors[multicolorPalette[currentBits]][1]; spriteColorLine [posInColorLine + 14] = colors[multicolorPalette[currentBits]][2]; spriteColorLine [posInColorLine + 15] = 255; } else { spriteColorLine [posInColorLine + 0] = 0; spriteColorLine [posInColorLine + 1] = 0; spriteColorLine [posInColorLine + 2] = 0; spriteColorLine [posInColorLine + 3] = 0; spriteColorLine [posInColorLine + 4] = 0; spriteColorLine [posInColorLine + 5] = 0; spriteColorLine [posInColorLine + 6] = 0; spriteColorLine [posInColorLine + 7] = 0; spriteColorLine [posInColorLine + 8] = 0; spriteColorLine [posInColorLine + 9] = 0; spriteColorLine [posInColorLine + 10] = 0; spriteColorLine [posInColorLine + 11] = 0; spriteColorLine [posInColorLine + 12] = 0; spriteColorLine [posInColorLine + 13] = 0; spriteColorLine [posInColorLine + 14] = 0; spriteColorLine [posInColorLine + 15] = 0; } posInColorLine = posInColorLine + 16; spriteData = (spriteData << 2) & 0xffffff; } }
Note the declaration of spriteColorLine as a global variable. If we left this declaration within each method this would cause a 192 byte array be allocated each time we draw a 8 pixel segment. This would potentially cause garbage collection to kick in every couple of seconds causing an irritating jerk in the emulator.
On a high level, each method basically does the following:
- Get sprite line data
- Define color pallette
- Loop thorugh the color data and outputting the applicable color from the color pallette
Glueing everything together
Time to glue everything together. This will involve completing the process sprites method:function processSprites() { var i = 0; var spriteBit = 0x80; var lineSegmentStart = cycleInLine << 3; var lineSegmentStop = lineSegmentStart | 7; for (i = 0; i < 8; i++) { var currentSprite = 7 - i; spriteBit = 0x80 >> i; if ((registers[0x15] & spriteBit) == 0) continue; var xExpanded = (registers[0x1d] & spriteBit) != 0; var yExpanded = (registers[0x17] & spriteBit) != 0; var xDimension = xExpanded ? 48: 24; var yDimension = yExpanded ? 42: 21; var spritePosX = registers[currentSprite << 1]; if ((registers[0x10] & spriteBit) != 0) { spritePosX = spritePosX | 0x100; } var spritePosY = registers[(currentSprite << 1) | 1]; if (!((cycleline >= spritePosY) & (cycleline < (spritePosY + yDimension)))) continue; var lineScenario = 0; if (((lineSegmentStart >= spritePosX) & (lineSegmentStart < (spritePosX + xDimension)))) lineScenario = 2; if (((lineSegmentStop >= spritePosX) & (lineSegmentStop < (spritePosX + xDimension)))) lineScenario = lineScenario | 1; if (lineScenario == 0) continue; var requiredLineInSprite = cycleline - spritePosY; if (yExpanded) requiredLineInSprite = requiredLineInSprite >> 1; var spriteIsMultiColor = (registers[0x1c] & spriteBit) != 0 if (xExpanded) { if (spriteIsMultiColor) { populateSpriteMultiColorLineExpanded(currentSprite, requiredLineInSprite); } else { populateSpriteColorLineExpanded(currentSprite, requiredLineInSprite); } } else { if (spriteIsMultiColor) { populateSpriteMultiColorLine(currentSprite, requiredLineInSprite); } else { populateSpriteColorLine(currentSprite, requiredLineInSprite); } } var canvasForSprite; if ((registers[0x1b] & spriteBit) == 0) { canvasForSprite = spriteForegroundData; } else { canvasForSprite = spriteBackgroundData; } var posInSpriteCanvas = posInCanvas - 8; if (lineScenario == 1) { var startInLineSeg = (spritePosX & 7) << 2; posInSpriteCanvas = posInSpriteCanvas + startInLineSeg; var i = 0; var posInSpriteColorLine = 0; for (i = startInLineSeg; i < 32; i = i + 4) { canvasForSprite.data[posInSpriteCanvas + 0] = spriteColorLine[posInSpriteColorLine+0]; canvasForSprite.data[posInSpriteCanvas + 1] = spriteColorLine[posInSpriteColorLine+1]; canvasForSprite.data[posInSpriteCanvas + 2] = spriteColorLine[posInSpriteColorLine+2]; canvasForSprite.data[posInSpriteCanvas + 3] = spriteColorLine[posInSpriteColorLine+3]; posInSpriteCanvas = posInSpriteCanvas + 4; posInSpriteColorLine = posInSpriteColorLine + 4; } } else if (lineScenario == 2) { var startInLineSeg = 0; var endInLineSeg = (spritePosX & 7) << 2; var i = 0; var posInSpriteColorLine = (xDimension - (spritePosX & 7)) << 2; for (i = 0; i < endInLineSeg; i = i + 4) { canvasForSprite.data[posInSpriteCanvas + 0] = spriteColorLine[posInSpriteColorLine+0]; canvasForSprite.data[posInSpriteCanvas + 1] = spriteColorLine[posInSpriteColorLine+1]; canvasForSprite.data[posInSpriteCanvas + 2] = spriteColorLine[posInSpriteColorLine+2]; canvasForSprite.data[posInSpriteCanvas + 3] = spriteColorLine[posInSpriteColorLine+3]; posInSpriteCanvas = posInSpriteCanvas + 4; posInSpriteColorLine = posInSpriteColorLine + 4; } } else { var i = 0; var posInSpriteColorLine = (lineSegmentStart - spritePosX) << 2; for (i = 0; i < 32; i = i + 4) { canvasForSprite.data[posInSpriteCanvas + 0] = spriteColorLine[posInSpriteColorLine+0]; canvasForSprite.data[posInSpriteCanvas + 1] = spriteColorLine[posInSpriteColorLine+1]; canvasForSprite.data[posInSpriteCanvas + 2] = spriteColorLine[posInSpriteColorLine+2]; canvasForSprite.data[posInSpriteCanvas + 3] = spriteColorLine[posInSpriteColorLine+3]; posInSpriteCanvas = posInSpriteCanvas + 4; posInSpriteColorLine = posInSpriteColorLine + 4; } } } }
Overall, the following is happening in this method:
- Figuring out applicable scenario fusing the truth table from a previous section. We basically form a two bit binary number from the two conditions
- Determining which line in the sprite to use. This is calculated by subtracting SpritePosY from cycleline. For Y-Expanded sprites, the result is divided by two.
- Get the spriteColoLine, considering whether the sprite is multicolor and/or XExpanded.
- Get hold of the correct sprite canvas to draw the data, by checking if sprite should shown in front or behind text.
- Do the actual drawing to the canvas using the line scenario as a selector
Test run and Debugging
With all this coding let us do some testing.When I started the game a couple of things went wrong.
Firstly all the sprites appeared as silhouettes.
Secondly, a very short moment after I try to move around, the emulator browser window freezes completely, not even allowing me to open up the inspector window.
After some backwards and forwards, I actually discovered that if I open the inspector window before the browser window freezes, the world goes smoother: I can actually pause execution and single step.
After some single stepping I discovered that the main loop in process sprites actually never makes it to the end. It will get up to the second last sprite, and then suddenly set the loop variable i to 5.
What is going on here?
The problem was actually caused by another inner loop, used for writing to the sprite canvas, also using variable i. This is actually where my Java experience counts against me. In Java, when you declare a variable within a code block, the variable will only be visible within that specific code block.
In JavaScript, that is not the case. Even if you declare a variable within a code block, it will still be visible for every line of code within the function.
So, let us fix the issue by changing the loop variables of the inner loops to a different name. I will use the name j.
Let us restart the game with these changes. This time, the world looks better:
There is still a bit of work left, though, which we will discuss in the next section.
Fixing the remaining glitches
We made lots of progress in the previous section. At last we can see sprites!From the screenshot in the previous section, we can see there is however still a bit of work to be done. Let us try to summarise our todo list:
- Vertical positioning: There appear to an issue with the vertical positioning of the sprites. Currently Dan Dare is walking on the title bar. In reality, however, he should be positioned a slightly higher.
- Horizontal positioning: There also appear to an issue with the horizontal positioning of sprites. This is evident from the screenshot below, where Dan Dare appears more to the left of the ladder.
- Colors of multi color text graphics are wrong: There is a couple of examples. Some areas are orange which should in fact be black.
- Sprites leaving traces as it moves. This is most noticeable in the movement of the small elephant.
- Lack of transparency when two sprites overlap. You can also see this from the screen shot in the previous section with the elephant standing behind Dan Dare.
We will now attend to this todo list in the following subsections.
Vertical and horisontal positioning
Let us start with first tackling the sprite vertical positioning issue. In a previous blog we made the assumption that the first visible line starts at raster line 42. But is this correct?
I skimped through the C64's Programmers reference manual and on page 139, I found the following:
The First Y value in which a sprite (expanded or not) is fully on the screen (all 21 possible lines displayed) is 50.This is our issue! First visible raster line should not be 42, but 50!
Let us see if the Programmers Reference Guide can also give us an answer to our horisontal positioning issue. Indeed it does.
If you go down further on page 139 to the section Horizontal Positioning:
Each sprite can have a position from 0 to 511. However, only those values between 24 and 343 are visible on the screen.So, the horizontal visible part of the screen starts at position 24. In effect our emulator starts at pixel position 32, so there is our problem!
So lets us fix the following methods:
... function isPixelArea() { var visibleColumn = (cycleInLine >= 4) & (cycleInLine < (4+40)); var visibleRow = (cycleline > 49) & (cycleline < (50 + 200)); return (visibleColumn & visibleRow); } ... function updateCharPos() { if ( !((cycleline > 49) & (cycleline < (50 + 200))) ) return; var lineInScreen = cycleline - 50; if (lineInScreen == 0) { charPosInMem = 0; return; } if ((lineInScreen & 7) == 0) { charPosInMem = charPosInMem + 40; } } ...
I just would like to highlight the fact that for horizontal positions our emulator work with character positions, which is pixel position divided by eight.
Multi color text mode issues
Lets investigate why some black areas in the multi color text mode plane appear as orange.Maybe a good place to start is to have a look a the color code for orange, which is eight.
This is interesting. The lower three bits of an eight is 0, which is the color code for black.
Some bells start to ring. Could it be that for multi color textmode, we should consider the bottom three bits? The resources I have consulted was very explicit about this. They only mentioned that in multi color text mode you are only allowed the colors 8-15.
The programers reference guide comes to our rescue once again. On page 117 there is a table giving the colors for the different two bit combinations for multicolor text mode. The interesting combination is three:
Color specified by the lower 3 bits in color memoryOur theory has just being confirmed! We need to modify drawTextModeMultiColor as follows:
function drawTextModeMultiColor(charPos) { ... var backgroundColor = registers[0x21]; var color1 = registers[0x22]; var color2 = registers[0x23]; var color3 = textColor & 7; var colorArray = [backgroundColor, color1, color2, color3]; ... }
Lack of transparency at Sprite overlap
Next, let us fix the lack of transparency when sprites overlap.This issue is caused by the fact, that we always draw all the pixels of a sprite line, irrespective of whether the pixel is transparent or not. We should, however, that all sprites share the same two canvasses.
Therefore, writing 4 zeros to each byte of pixel of a sprite canvas will expose the pixel in a canvas below, but will overwite the pixel of the sprite below it, that was supposed to be visible!
So, in effect what we should be doing is to to just leave a pixel alone that should be transparent. For each segment of sprite canvas population, we need add a canvas:
if (lineScenario == 1) { var startInLineSeg = (spritePosX & 7) << 2; posInSpriteCanvas = posInSpriteCanvas + startInLineSeg; var j = 0; var posInSpriteColorLine = 0; for (j = startInLineSeg; j < 32; j = j + 4) { if (spriteColorLine[posInSpriteColorLine+3] != 0) { canvasForSprite.data[posInSpriteCanvas + 0] = spriteColorLine[posInSpriteColorLine+0]; canvasForSprite.data[posInSpriteCanvas + 1] = spriteColorLine[posInSpriteColorLine+1]; canvasForSprite.data[posInSpriteCanvas + 2] = spriteColorLine[posInSpriteColorLine+2]; canvasForSprite.data[posInSpriteCanvas + 3] = spriteColorLine[posInSpriteColorLine+3]; } posInSpriteCanvas = posInSpriteCanvas + 4; posInSpriteColorLine = posInSpriteColorLine + 4; } } else if (lineScenario == 2) { var startInLineSeg = 0; var endInLineSeg = (spritePosX & 7) << 2; var j = 0; var posInSpriteColorLine = (xDimension - (spritePosX & 7)) << 2; for (j = 0; j < endInLineSeg; j = j + 4) { if (spriteColorLine[posInSpriteColorLine+3] != 0) { canvasForSprite.data[posInSpriteCanvas + 0] = spriteColorLine[posInSpriteColorLine+0]; canvasForSprite.data[posInSpriteCanvas + 1] = spriteColorLine[posInSpriteColorLine+1]; canvasForSprite.data[posInSpriteCanvas + 2] = spriteColorLine[posInSpriteColorLine+2]; canvasForSprite.data[posInSpriteCanvas + 3] = spriteColorLine[posInSpriteColorLine+3]; } posInSpriteCanvas = posInSpriteCanvas + 4; posInSpriteColorLine = posInSpriteColorLine + 4; } } else { var j = 0; var posInSpriteColorLine = (lineSegmentStart - spritePosX) << 2; for (j = 0; j < 32; j = j + 4) { if (spriteColorLine[posInSpriteColorLine+3] != 0) { canvasForSprite.data[posInSpriteCanvas + 0] = spriteColorLine[posInSpriteColorLine+0]; canvasForSprite.data[posInSpriteCanvas + 1] = spriteColorLine[posInSpriteColorLine+1]; canvasForSprite.data[posInSpriteCanvas + 2] = spriteColorLine[posInSpriteColorLine+2]; canvasForSprite.data[posInSpriteCanvas + 3] = spriteColorLine[posInSpriteColorLine+3]; } posInSpriteCanvas = posInSpriteCanvas + 4; posInSpriteColorLine = posInSpriteColorLine + 4; }
This approach might cause some residue, so lets play safe and add some functionality to clear the two sprite canvasses in the beginning of each frame.
First a method within our video class:
this.initForNextFrame = function() { var i; for (i = 0; i < spriteBackgroundData.data.length; i++) { spriteBackgroundData.data[i] = 0; } for (i = 0; i < spriteForegroundData.data.length; i++) { spriteForegroundData.data[i] = 0; } }
Next, we should invoke this method within runBatch:
function runBatch() { if (!running) return; mycpu.setAllowLogging(document.getElementById("allowDebug").checked); myvideo.initForNextFrame(); //myvideo.updateCanvas(); //var targetCycleCount = mycpu.getCycleCount() + 20000; while (true) { ... } ... }
This set of changes will also take care of the problem where sprite movement leave a trace on the screen.
Final Results
With the bugs fixed mentioned in this blog, Dan Dare is now in a fully playable state. Here is a couple of screen shots:Just to refresh your mind, the github io page I have creataed for this series of blogs was ovalcode.github.io
I haven't updated this io page for some time until this blog post. So, you can now play with my JavaScript emulator without cloning the source and starting a web server.
As an added bonus, I have added some functionality to this io page which will allow you to use the emulator on a touch device like a tablet. This include just enough letters enabling you to kick off the loading of a tape image. This will however be slow on a mobile device than on an actual PC.
To give you an idea of the speed, I have added a frame per second counter on top of screen of the emulator. On a Intel i7 I managed to get about the real speed of 50 frames per second.
On an Intel i3 and i5 I get between 40 and 45 frames per second.
The best I could get on a mobile device was 9 frames per second.
In Summary
This blog was the concluding post on my series of blog posts on writing a JavaScript emulator from scratch.
In this blog we implemented sprites and ended off with our emulator been able to fully play Dan Dare.
Thanks for everyone that read my series of blog posts.
I am thinking of writing a similar set of blog posts in a couple of months time, where I will be implementing an emulator on Android.
Take care!
Amazing work Johan...amazing!
ReplyDeleteAmazing work Johan...amazing!
ReplyDeleteThanks Stiaan!
DeleteThanks Stiaan!
Delete