Showing posts with label Atari. Show all posts
Showing posts with label Atari. Show all posts

Saturday, September 14, 2019

TIA Sprite Implementation

Now that we have a sprite class for the internal processing of the TIA chip Player Missile Graphics (which I will refer to as sprites), we need to implement the TIA registers that handle the various sprite activity and adjust our sprites to reflect the register settings. First, we create the 5 sprites that the TIA uses. This is done in an array class variable with constants set up in the TIAPIARegs singleton class that hold the index values of each sprite.

val sprites = arrayOf(PlayerMissileGraphic(1, 30570), // Ball
PlayerMissileGraphic(8, 15423), // Player 1
PlayerMissileGraphic(1, 1023), // Missile 1
PlayerMissileGraphic(8, 28377), // Player 0
PlayerMissileGraphic(1, 24007) ) // Missile 0

The most important thing for a sprite is to be able to be shown. The ball and missiles have an enable register which when bit 1 is set will show the sprite otherwise will hide the sprite. For the player sprites, set the byte that represents the image to 0 for the sprite not to be drawn and fill it with the display data to have the sprite draw something.

TIAPIARegs.ENABL -> sprites[TIAPIARegs.ISPRITE_BALL].drawingBits = if ((value and 2) == 2) 1 else 0
TIAPIARegs.ENAM0 -> sprites[TIAPIARegs.ISPRITE_MISSILE0].drawingBits = if ((value and 2) == 2) 1 else 0
TIAPIARegs.ENAM1 -> sprites[TIAPIARegs.ISPRITE_MISSILE1].drawingBits = if ((value and 2) == 2) 1 else 0
TIAPIARegs.GRP0 -> sprites[TIAPIARegs.ISPRITE_PLAYER0].drawingBits = value and 255
TIAPIARegs.GRP1 -> sprites[TIAPIARegs.ISPRITE_PLAYER1].drawingBits = value and 255

Being able to see the sprite is nice, but we need to control where on the scan-line the sprite will be displayed. It would be nice if we just had a register that held the position information of the sprites, but for some reason that is beyond me this is not how sprite positioning works on the TIA. To position a sprite you simply write anything into the RESXX register and it will position the sprite at the current color clock position or position 0 if called in the horizontal blanking interval.

TIAPIARegs.RESBL -> sprites[TIAPIARegs.ISPRITE_BALL].x = column
TIAPIARegs.RESP0 -> sprites[TIAPIARegs.ISPRITE_PLAYER0].x = column
TIAPIARegs.RESP1 -> sprites[TIAPIARegs.ISPRITE_PLAYER1].x = column
TIAPIARegs.RESM0 -> sprites[TIAPIARegs.ISPRITE_MISSILE0].x = column
TIAPIARegs.RESM1 -> sprites[TIAPIARegs.ISPRITE_MISSILE1].x = column
TIAPIARegs.RESMP0 -> sprites[TIAPIARegs.ISPRITE_MISSILE0].x = sprites[TIAPIARegs.ISPRITE_PLAYER0].x
TIAPIARegs.RESMP1 -> sprites[TIAPIARegs.ISPRITE_MISSILE1].x = sprites[TIAPIARegs.ISPRITE_PLAYER1].x

The problem with this is that each CPU cycle is 3 color clock cycles and a STA instruction takes at least 3 cycles to complete. This means that we need to adjust the horizontal position using the move delta. This is done at the start of the scanline by writing to HMOVE with any value. The amount each sprite moves is a signed nibble set by HMxx registers with a HMCLR register that sets all the HMxx registers to zero. We have a utility method to convert a nibble into a signed integer as follows

private fun convertNibbleToSignedInt(nibble:Int):Int {
val nib = nibble and 15
return if (nib > 7) nib - 16 else nib
}

And within the writeRegister when statement we have the following registers implemented:

TIAPIARegs.HMP0 -> sprites[TIAPIARegs.ISPRITE_PLAYER0].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMP1 -> sprites[TIAPIARegs.ISPRITE_PLAYER1].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMM0 -> sprites[TIAPIARegs.ISPRITE_MISSILE0].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMM1 -> sprites[TIAPIARegs.ISPRITE_MISSILE1].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMBL -> sprites[TIAPIARegs.ISPRITE_BALL].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMOVE -> {
for (cntr in 0..4)
sprites[cntr].hmove()
}
TIAPIARegs.HMCLR -> {
for (cntr in 0..4)
sprites[cntr].deltaX = 0
}

Sprite scaling and mirroring is done through the NUSIZx register with the copy/scale mode of the player sprites being the lower 3 bits and the scaling of missile for that player being two to the power of the two bit number occupying bits 4 and 5. In other words, we take the last three bits to form a number between 0 and 7 which is used to set the player sprite scaling and copies via the setePlayerScaleCopy method that we wrote in the previous article. The missile scaling is bits 4 and 5 which when shifted right become a number between 0 and 3, this is used as a power of two which can be done by left-shifting a 1 by the given number which would result in 1, 2, 4, or 8.

TIAPIARegs.NUSIZ0 -> {
sprites[TIAPIARegs.ISPRITE_PLAYER0].setPlayerScaleCopy(value and 7)
sprites[TIAPIARegs.ISPRITE_MISSILE0].scale = 1 shl ((value shr 4) and 3)
}
TIAPIARegs.NUSIZ1 -> {
sprites[TIAPIARegs.ISPRITE_PLAYER1].setPlayerScaleCopy(value and 7)
sprites[TIAPIARegs.ISPRITE_MISSILE1].scale = 1 shl ((value shr 4) and 3)
}

Finally, mirroring is simply setting the sprite mirror variable based on the state of bit 3 of the value passed to the REFx register.

TIAPIARegs.REFP0 -> sprites[TIAPIARegs.ISPRITE_PLAYER0].mirror = (value and 8) == 8
TIAPIARegs.REFP1 -> sprites[TIAPIARegs.ISPRITE_PLAYER1].mirror = (value and 8) == 8

That just about covers all the write registers for manipulating sprites, but there are also a set of eight readable registers that are used for handling collision detection. This is a bit complex of a subject, though is a lot easier to use than one would expect and will be covered next time.

Saturday, August 31, 2019

Sprites are Player Missile Graphics

To plan out the classes that I would use for creating the player and missile sprites I put together the following table which shows the various registers used for the different player and missiles as well as how the different objects compare to each other. Table 1 is a handy chart to have for programming the 2600 and wish I would have created it when I first started playing around with the 2600!


The original plan was to have a base class that held the common functionality and have the player class and missile/ball classes override this class. This is probably the “proper” way to do this but the classes are so similar that having them combined into a single class seemed like the easiest solution. A missile or ball is essentially a sprite that is only 1 pixel so by simply tracking the number of pixels as size, all the different player missile graphics can be incorporated.

Sprites have a collision mask that will be used for determining which collision flags they are part of. This will be explained later when we implement collisions but for now it can be thought of as simply a magic number with each bit in the number representing a particular pair of objects that can collide with each other.

Scale is how many pixels to draw for each pixel in the sprite. The 2600 supports scales of 1, 2, 4, and 8 with player sprites limited to a maximum scale of 4. The number of copies is how many times to draw the sprite while the gap is the distance (in multiples of the size) between the copies. The 2600 supports distances of 2, 4 and 8.

The sprite is placed on the screen at the x location with the deltaX variable being how much to alter the x position when an HMOVE command is issued. Normally we do not want the sprite to move so the default is 0.

The drawing bits are the byte used to represent the image in the sprite at that horizontal scanline. It can be considered a bit of a kludge when using missiles and balls as only the one bit is used. Mirroring is used to indicate which order the pixels should be drawn. For balls and missiles it will mirror the single pixel but as the result of that is the original image there is not much use for size 1 sprites.

class PlayerMissileGraphic(var size:Int, var collisionMask:Int) {
var scale = 1
var copies = 1
var distanceBetweenCopies = 0   // close = 2, med = 4 far = 8
var x = 0
var deltaX = 0
var drawingBits = 0
var delayedDraw = false
var mirror = false

The heart of the sprite class is the isPixelDrawn method which simply determines if the current state of the sprite would result in a pixel being drawn at the indicated location. This is simply a matter of looping through the number of copies of the sprite and for each copy being drawn figure out where it will be drawn. We know the number of pixels that a copy will occupy as it is simply the size times the scale so if the column being drawn is within that range we figure out which pixel in the image is at that location and if that bit is 1 then we are drawing otherwise we are not. Calculating which bit in the drawing data to draw depends on whether we are mirroring the sprite and the scale of the pixels.

fun isPixelDrawn(col:Int):Boolean {
if (col < x) return false
if (delayedDraw) return false

var pixelAtCol = false
for (copyCount in 1..copies) {
val copyStart = x + (copyCount - 1) * size * scale * distanceBetweenCopies
val copyEnd = copyStart + size * scale
if ((col >= copyStart) and (col < copyEnd)) {
val bitPos = (col - copyStart) / scale
val bit = if (mirror) 1 shl bitPos else (1 shl (size-1)) ushr bitPos
if ((bit and drawingBits) > 0) pixelAtCol = true
}
}

return pixelAtCol
}


The HMOVE command is processed simply by adjusting the x variable by the movement delta with a bit of clamping code to make sure it is within the range of the display. My clamping is done by nesting a max function within a min function by having the highest column be the minimum to compare against and 0 and the target column being the max to pick from. As a result the max will clip away negative values making them 0 while the min will clip away off-right-edge values making them the right edge.

fun hmove() {
x = min(159, max(0, x + deltaX))
}


The TIA player sprites have 8 different modes as shown by the chart below. While this could be handled by the TIA register calls, there would be duplication of code so I wrote a simple utility method that will set the sprite scale, copies, and distance based on a player sprite mode. This method is just a simple when statement which sets the parameters appropriately based on the indicated mode. The different play modes that the TIA recognizes is in table 2.



fun setPlayerScaleCopy(scmode:Int) {
when (scmode) {
TIAPIARegs.PMG_NORMAL ->
{scale = 1; copies = 1; distanceBetweenCopies = 1}
TIAPIARegs.PMG_TWO_CLOSE  ->
{scale = 1; copies = 2; distanceBetweenCopies = 2}
TIAPIARegs.PMG_TWO_MEDIUM  ->
{scale = 1; copies = 2; distanceBetweenCopies = 4}
TIAPIARegs.PMG_THREE_CLOSE ->
{scale = 1; copies = 3; distanceBetweenCopies = 2}
TIAPIARegs.PMG_TWO_WIDE  ->
{scale = 1; copies = 2; distanceBetweenCopies = 8}
TIAPIARegs.PMG_DOUBLE_SIZE  ->
{scale = 2; copies = 1; distanceBetweenCopies = 1}
TIAPIARegs.PMG_THREE_MEDIUM  ->
{scale = 1; copies = 3; distanceBetweenCopies = 4}
TIAPIARegs.PMG_QUAD_PLAYER  ->
{scale = 4; copies = 1; distanceBetweenCopies = 1}
}
}

We now have a class that can handle the sprites in the TIA but we still have to handle the TIA registers related to drawing the sprites and perform the drawing of the sprites when the color clock reaches them. This will be what we do next.

Saturday, August 17, 2019

The 2600 Play-field

The play-field is a 40-bit image that is stretched across the 160 pixels that make up a scan-line. This means that each play-field bit occupies 4 pixels. The original purpose behind the play-field is to create the map or maze that the player occupies. It is often used for other things such as drawing scores, background images or title screens. Anything that can be represented using crude graphics is fine.

The play-field is managed using the CTRLPF, PF0, PF1 and PF2 registers. The CTRLPF register controls how the play-field will be displayed controlling the display order of the play-field, whether the bits should be repeated or mirrored, and if the play-field should be drawn using the play-field color or drawn using player 0 color on the left and player 1 color on the right. This register also handles scaling of the ball which will be covered later.

The PF0, PF1 and PF2 registers hold the 20 bits that are to be drawn. These bits will either be repeated or mirrored based on the settings in CTRLPF. As with color, you can change the play-field bits in the middle of drawing so that the right side of the screen is different from the left side of the screen. The order of the bits is weird which is likely a by-product of keeping the transistor count of the chip down. The image below shows the order of the pixels that make up the display.



Essentially the play-field drawing starts with the 4 bits in the PF0 register drawn in low to high order. This is followed by the eight bits from PF1 in high to low order. Finally the PF2 bits are drawn in low to high order.  This is repeated or mirrored for the right half of the screen. As many games used symmetrical maps this made it easy to set up the play-field bits and simply mirror them. Asymmetric maps are a bit trickier as you need to set up the PF0, PF1, and PF2 registers for the left half of the screen then wait until the beam is past PF0 to change PF0 to the right side PF0, wait until the beam is past PF1 to change to the right side PF1 and wait until the beam is past PF2 to change to the right side PF2. While you could theoretically do this while mirroring, asymmetrical images are easier to do with repeated mode unless you don’t need to change PF2.

The hard part about implementing the play-field is dealing with the different orientations of the data. My internal representation of the play-field data is simply a 20 bit integer with the bits being the play-field bits from left to right. When one of the PF registers is called, the appropriate bits of this play-field value are set. This requires that we flip the bits, but this is a simple process of using and to see if a bit is set or to build the reversed byte and shifts to get the bits aligned properly.

fun reversePFBits(value:Int):Int {
var reversed = 0
var testBit = 1
for (cntr in 0..7) {
reversed *= 2
reversed += if ((testBit and value) > 0) 1 else 0
testBit *= 2
}
return reversed
}

The calls to the playfield register simply take the value that is to be written to the register and, if necessary, reverse the bits. It then masks out the bits in the playfield that the register represents, shifts the bits to the appropriate part of the playfield then uses a logical or to put the new value in place. The playfield control register CTRLPF simply sets flags to indicate how the playfield should be rendered. The flags are mirror to indicate if mirroring should be taking place, scoreMode to determine if we are drawing using the playfield color or the player 0 color on the left and the player 1 color on the right. The priorityPlayfield flag determines if the playfield should be drawn first or last.

TIAPIARegs.PF0 -> {
var bits = reversePFBits(value)
playfieldBits = (playfieldBits and 0xFFFF) or (bits shl(16))
}
TIAPIARegs.PF1 -> {
var bits = value
playfieldBits = (playfieldBits and 0xF00FF) or (bits shl(8))
}
TIAPIARegs.PF2 -> {
var bits = reversePFBits(value)
playfieldBits = (playfieldBits and 0xFFF00) or bits
}
TIAPIARegs.CTRLPF -> {
mirror = (value and 1) == 1             // bit 0 handles mirror mode
scoreMode = (value and 2) == 2          // bit 1 handles score mode
priorityPlayfield = (value and 4) == 4  // bit 2 Places playfield above sprites
}

To draw the play-field we simply determine which play-field pixel should be processed. The left is simply the column being drawn divided by 4. The right is the same (less 80) if repeated and the inverse if mirrored. Once we know the bit to be checked we can determine if there is a pixel there and if so set the color based on the scoreMode. We then adjust our collision mask which will be explained in the collisions section later but is essentially a set of bits indicating which collisions have happened.


val pfCol = if (column < 80) column / 4 else
if (mirror) (159 - column) / 4 else (column - 80) / 4
var pfPixelMask = 0x80000 shr pfCol
val shouldDrawPlayfield =(playfieldBits and pfPixelMask) > 0
val pfColor = if (scoreMode) {if (column < 80) playerMissile0Color else
playerMissile1Color } else playfieldBallColor
if ( ! shouldDrawPlayfield) collisionMask = collisionMask and 31668


At the start of the drawing of the pixel, we check to see if the playfield is normal priority and if so draw it.

if ((shouldDrawPlayfield) and ( ! priorityPlayfield )) pixelColor = pfColor
// TODO sprite drawing code here 
if ((shouldDrawPlayfield) and (priorityPlayfield)) pixelColor = pfColor


Finally, at the end of the drawing of a pixel, we see if the playfield is priority and if so draw it. Between these two checks is the sprite drawing which is the next thing to be implemented.

Saturday, August 3, 2019

Drawing Pixels on 2600 TIA

In the 2600, the TIA is drawing the display in real time by sending signals to the television. There was no frame buffer in the TIA. With an emulator, our situation is slightly different. As I want the TIA emulator to work in several different platforms, a cross-platform way of generating the display is needed. My solution is to simply have a 160 “pixel” raster line that the TIA emulator writes to. Whenever a scan-line is finished, or part way through drawing the scan-line if you are doing some type of stepping through the program, the class managing the display will grab this information and use the TIAColors information to build a display appropriate to the platform that is being rendered.

The figure below shows the logic that the TIA uses to determine which of the four colors to place on a pixel. We start with the background color. This would be the background color at the time that the pixel was drawn. As the color registers can be changed in the middle of rendering a line this can be a distinct color at different points along the scan-line.



The order that things are drawn depends on flags in the CTRLPF register. Normally the play-field is drawn right after the background but if bit 1 of the CTRLPF is set then the play-field and ball will be drawn after the players and missiles. Player 1 and the missile associated with that player is drawn before player 0. So if CTRLPF bit 1 is set the drawing order is background, player and missile 1, player and missile 0, play-field and ball. Otherwise the drawing order is background, play-field and ball , player and missile 1, player and missile 0.

This drawing order determines what the final pixel color will be in cases where multiple display objects overlap a given pixel. The 2600 goes even further by tracking the overlaps and setting a series of collision flags that can be tested to see if there were any collision between display objects when the line was being drawn.

This effectively means that we have six things to potentially draw in any given pixel. Three of these --  the ball, missile 0, and missile 1 – are simply a line so they can be grouped into a common class. The two player sprites are not that much more complicated so while they could be a separate class from the ball and missile, making a more complicated player missile graphics class to handle all five of these display objects makes sense.

This leaves us with the play-field. This is my favorite part of the 2600 rasterizer as it is the closest thing you have to traditional bitmap graphics.

Saturday, July 20, 2019

Color Me TIA

One of the big, if not the biggest, challenge of programming the 2600 was simply getting the scan-lines to be rendered properly. You are drawing the display as it is being shown and only have 76 cycles per scan line to set up the registers. As with many graphics processors, the TIA can be thought of as a state machine with the values contained in the registers at the end of a scan line being retained at the start of the next scan-line. This fact must be considered when writing your renderer, known as a kernel in 2600 parlance. A well-designed kernel will take advantage of this fact while graphical artifacts will result from a poorly designed kernel.

Quick math shows that 160 pixels with only 76 cycles means that more than a single pixel is being drawn per CPU cycle. The TIA has its own clock which runs three times the speed of the CPU. This means that there are 228 TIA cycles per scan-line. To distinguish TIA cycles from CPU cycles, the TIA cycles have been given the name color clocks. The first 68 of these color clock ticks are used for horizontal blanking so do not draw anything to the screen but does give the kernel time to set up some of the TIA registers. The remaining 160 color clocks draw the pixels of the display based on the register settings at the time a pixel is being drawn.

Pixels can be one of four colors based on what is being drawn in that location. The background color is what is used if there is nothing to draw in that location. The play-field color is the color of the play-field and the ball. Finally each player and their missile have a color.

The colors are selected from a palette of 128 colors by simply setting the appropriate color register to the desired color. The color byte is in the format HHHHLLLX with the 4 H bits being the Hue, the L bits being the Luminosity, and X being unused. The hue is the base color of the pixel while the luminosity is the brightness of the pixel. While the color could be calculated, with only 128 colors it is much quicker just to have a table of colors.

class TIAColors() {
val colorTable:Array<Int> = arrayOf(
// Hue 0
0xFF000000.toInt(),0xFF000000.toInt(),0xFF404040.toInt(), 0xFF404040.toInt(),0xFF6c6c6c.toInt(), 0xFF6c6c6c.toInt(),0xFF909090.toInt(), 0xFF909090.toInt(),
0xFFb0b0b0.toInt(),0xFFb0b0b0.toInt(),0xFFc8c8c8.toInt(),0xFFc8c8c8.toInt(),
0xFFdcdcdc.toInt(),0xFFdcdcdc.toInt(),0xFFececec.toInt(),0xFFececec.toInt(),
// Hue 1
0xFF444400.toInt(),0xFF444400.toInt(),0xFF646410.toInt(),0xFF646410.toInt(),
0xFF848424.toInt(),0xFF848424.toInt(),0xFFa0a034.toInt(),0xFFa0a034.toInt(),
0xFFb8b840.toInt(),0xFFb8b840.toInt(),0xFFd0d050.toInt(),0xFFd0d050.toInt(),
0xFFe8e85c.toInt(),0xFFe8e85c.toInt(),0xFFfcfc68.toInt(),0xFFfcfc68.toInt(),
//Hue 2
0xFF702800.toInt(),0xFF702800.toInt(),0xFF844414.toInt(),0xFF844414.toInt(),
0xFF985c28.toInt(),0xFF985c28.toInt(),0xFFac783c.toInt(),0xFFac783c.toInt(),
0xFFbc8c4c.toInt(),0xFFbc8c4c.toInt(),0xFFcca05c.toInt(),0xFFcca05c.toInt(),
0xFFdcb468.toInt(),0xFFdcb468.toInt(),0xFFecc878.toInt(),0xFFecc878.toInt(),
// Hue 3
0xFF841800.toInt(),0xFF841800.toInt(),0xFF983418.toInt(),0xFF983418.toInt(),
0xFFac5030.toInt(),0xFFac5030.toInt(),0xFFc06848.toInt(),0xFFc06848.toInt(),
0xFFd0805c.toInt(),0xFFd0805c.toInt(),0xFFe09470.toInt(),0xFFe09470.toInt(),
0xFFeca880.toInt(),0xFFeca880.toInt(),0xFFfcbc94.toInt(),0xFFfcbc94.toInt(),
// Hue 4
0xFF880000.toInt(),0xFF880000.toInt(),0xFF9c2020.toInt(),0xFF9c2020.toInt(),
0xFFb03c3c.toInt(),0xFFb03c3c.toInt(),0xFFc05858.toInt(),0xFFc05858.toInt(),
0xFFd07070.toInt(),0xFFd07070.toInt(),0xFFe08888.toInt(),0xFFe08888.toInt(),
0xFFeca0a0.toInt(),0xFFeca0a0.toInt(),0xFFfcb4b4.toInt(),0xFFfcb4b4.toInt(),
// Hue 5
0xFF78005c.toInt(),0xFF78005c.toInt(),0xFF8c2074.toInt(),0xFF8c2074.toInt(),
0xFFa03c88.toInt(),0xFFa03c88.toInt(),0xFFb0589c.toInt(),0xFFb0589c.toInt(),
0xFFc070b0.toInt(),0xFFc070b0.toInt(),0xFFd084c0.toInt(),0xFFd084c0.toInt(),
0xFFdc9cd0.toInt(),0xFFdc9cd0.toInt(),0xFFecb0e0.toInt(),0xFFecb0e0.toInt(),
// Hue 6
0xFF480078.toInt(),0xFF480078.toInt(),0xFF602090.toInt(),0xFF602090.toInt(),
0xFF783ca4.toInt(),0xFF783ca4.toInt(),0xFF8c58b8.toInt(),0xFF8c58b8.toInt(),
0xFFa070cc.toInt(),0xFFa070cc.toInt(),0xFFb484dc.toInt(),0xFFb484dc.toInt(),
0xFFc49cec.toInt(),0xFFc49cec.toInt(),0xFFd4b0fc.toInt(),0xFFd4b0fc.toInt(),
// Hue 7
0xFF140084.toInt(),0xFF140084.toInt(),0xFF302098.toInt(),0xFF302098.toInt(),
0xFF4c3cac.toInt(),0xFF4c3cac.toInt(),0xFF6858c0.toInt(),0xFF6858c0.toInt(),
0xFF7c70d0.toInt(),0xFF7c70d0.toInt(),0xFF9488e0.toInt(),0xFF9488e0.toInt(),
0xFFa8a0ec.toInt(),0xFFa8a0ec.toInt(),0xFFbcb4fc.toInt(),0xFFbcb4fc.toInt(),
// Hue 8
0xFF000088.toInt(),0xFF000088.toInt(),0xFF1c209c.toInt(),0xFF1c209c.toInt(),
0xFF3840b0.toInt(),0xFF3840b0.toInt(),0xFF505cc0.toInt(),0xFF505cc0.toInt(),
0xFF6874d0.toInt(),0xFF6874d0.toInt(),0xFF7c8ce0.toInt(),0xFF7c8ce0.toInt(),
0xFF90a4ec.toInt(),0xFF90a4ec.toInt(),0xFFa4b8fc.toInt(),0xFFa4b8fc.toInt(),
// Hue 9
0xFF00187c.toInt(),0xFF00187c.toInt(),0xFF1c3890.toInt(),0xFF1c3890.toInt(),
0xFF3854a8.toInt(),0xFF3854a8.toInt(),0xFF5070bc.toInt(),0xFF5070bc.toInt(),
0xFF6888cc.toInt(),0xFF6888cc.toInt(),0xFF7c9cdc.toInt(),0xFF7c9cdc.toInt(),
0xFF90b4ec.toInt(),0xFF90b4ec.toInt(),0xFFa4c8fc.toInt(),0xFFa4c8fc.toInt(),
// Hue 10
0xFF002c5c.toInt(),0xFF002c5c.toInt(),0xFF1c4c78.toInt(),0xFF1c4c78.toInt(),
0xFF386890.toInt(),0xFF386890.toInt(),0xFF5084ac.toInt(),0xFF5084ac.toInt(),
0xFF689cc0.toInt(),0xFF689cc0.toInt(),0xFF7cb4d4.toInt(),0xFF7cb4d4.toInt(),
0xFF90cce8.toInt(),0xFF90cce8.toInt(),0xFFa4e0fc.toInt(),0xFFa4e0fc.toInt(),
// Hue 11
0xFF003c2c.toInt(),0xFF003c2c.toInt(),0xFF1c5c48.toInt(),0xFF1c5c48.toInt(),
0xFF387c64.toInt(),0xFF387c64.toInt(),0xFF509c80.toInt(),0xFF509c80.toInt(),
0xFF68b494.toInt(),0xFF68b494.toInt(),0xFF7cd0ac.toInt(),0xFF7cd0ac.toInt(),
0xFF90e4c0.toInt(),0xFF90e4c0.toInt(),0xFFa4fcd4.toInt(),0xFFa4fcd4.toInt(),
// Hue 12
0xFF003c00.toInt(),0xFF003c00.toInt(),0xFF205c20.toInt(),0xFF205c20.toInt(),
0xFF407c40.toInt(),0xFF407c40.toInt(),0xFF5c9c5c.toInt(),0xFF5c9c5c.toInt(),
0xFF74b474.toInt(),0xFF74b474.toInt(),0xFF8cd08c.toInt(),0xFF8cd08c.toInt(),
0xFFa4e4a4.toInt(),0xFFa4e4a4.toInt(),0xFFb8fcb8.toInt(),0xFFb8fcb8.toInt(),
// Hue 13
0xFF143800.toInt(),0xFF143800.toInt(),0xFF345c1c.toInt(),0xFF345c1c.toInt(),
0xFF507c38.toInt(),0xFF507c38.toInt(),0xFF6c9850.toInt(),0xFF6c9850.toInt(),
0xFF84b468.toInt(),0xFF84b468.toInt(),0xFF9ccc7c.toInt(),0xFF9ccc7c.toInt(),
0xFFb4e490.toInt(),0xFFb4e490.toInt(),0xFFc8fca4.toInt(),0xFFc8fca4.toInt(),
// Hue 14
0xFF2c3000.toInt(),0xFF2c3000.toInt(),0xFF4c501c.toInt(),0xFF4c501c.toInt(),
0xFF687034.toInt(),0xFF687034.toInt(),0xFF848c4c.toInt(),0xFF848c4c.toInt(),
0xFF9ca864.toInt(),0xFF9ca864.toInt(),0xFFb4c078.toInt(),0xFFb4c078.toInt(),
0xFFccd488.toInt(),0xFFccd488.toInt(),0xFFe0ec9c.toInt(),0xFFe0ec9c.toInt(),
// Hue 15
0xFF442800.toInt(),0xFF442800.toInt(),0xFF644818.toInt(),0xFF644818.toInt(),
0xFF846830.toInt(),0xFF846830.toInt(),0xFFa08444.toInt(),0xFFa08444.toInt(),
0xFFb89c58.toInt(),0xFFb89c58.toInt(),0xFFd0b46c.toInt(),0xFFd0b46c.toInt(),
0xFFe8cc7c.toInt(),0xFFe8cc7c.toInt(),0xFFfce08c.toInt(),0xFFfce08c.toInt()
)

fun getARGB(indx:Int):Int {
if ((indx < 0) or (indx > 255))
return 0
else
return colorTable[indx]
}

fun getHTMLColor(indx:Int):String {
val noAlphaColor = getARGB(indx) and 0xFFFFFF
return "#${noAlphaColor.toString(16)}"
}
}

To aid in the selection of color values, my TIA testing program has a palette picker as seen in figure below.



This leaves the color clock. A simple function for handling a clock tick is simple enough to implement. I simply determine the pixel being rendered by subtracting the horizontal sync time from the color clock tick. If the column is negative, we don’t do anything otherwise we process the pixel at that column. The processing of the pixel will be implemented over the next several articles. Once the pixel is processed, we increment the color clock. If the color clock has reached it’s end we reset the color clock to zero and return true to indicate to the caller that the scanline is finished.

fun nextClockTick():Boolean {
// run current pixel
val column = colorClock - 68
if (column >= 0) {
var pixelColor = backgroundColor

// TODO render playfield
// TODO render player-missile graphics and set collisions

rasterLine[column] = pixelColor
}
++colorClock
return if (colorClock >= 228) {
colorClock = 0
true
} else false
}


Setting the colors is very simple as you simply set the appropriate color register to the desired value. These registers are COLUBK for the background color, COLUPF for the play-field and ball color, COLUP0 for player 0 and missile 0, and COLUP1 for player 1 and missile 1.

fun writeRegister(address:Int, value:Int) {
  when (address) {
    TIAPIARegs.COLUBK -> backgroundColor = value
    TIAPIARegs.COLUPF -> playfieldBallColor = value
    TIAPIARegs.COLUP0 -> playerMissile0Color = value
    TIAPIARegs.COLUP1 -> playerMissile1Color = value
    else -> println("TIA register $address not implemented!")
  }
}

Saturday, June 22, 2019

ITS TIA TIME!

We have a processor so now we want a way of displaying information to the user. The Atari 2600 used a custom chip called the Television Interface Adapter or TIA for displaying things. I have programmed graphics for a variety of consoles and computers but the TIA is nothing like those other devices. The NES, GameBoy, and Commodore 64 used a tile set approach to graphics where you had 256 8x8 characters that you would write to a grid that formed the screen display. The Amiga and Macintosh used bitmap graphics where you had a grid of pixels (sometimes broken into play-fields) that you would draw to. The PC had both depending on which graphics mode you had the graphics card in. The Atari 2600 does none of this!

The TIA has no video memory. The closest it comes to video memory is the play-field bits and sprite bits. In fact, the 128 bytes of RAM you have is less than the 160 pixels that can be displayed on a single line of the display. So, you are probably wondering, how do you display a screen on the 2600? You draw it in real time as the display is being generated. You have 76 processor cycles per line to set up the TIA registers to get it to display what you want. Some programmers have described programming the TIA as “racing the beam” which also happens to be the name of the book that got me interested in programming the 2600.

In North America, the television standard back then was NTSC. Other countries used different formats which meant they had different versions of the TIA and different restrictions. The programmer was responsible for the display which started with a 3 scan-line syncing signal followed by 37 vertical blank lines. These lines gave the programmer time to do some non-display work. The 192 lines that followed were the display. An additional 30 over scan lines were then drawn. These lines could appear on some TVs but generally you were to make these lines blank for compatibility. This provided a bit more time for non-display work.

If things sounded tricky, it gets worse. At the time, chips were expensive to manufacture so the designers of the chip were more focused on keeping the size of the chip and number of transistors as small as possible to maximize the yield during manufacturing. This meant that things like keeping the bits in the play-field registers the same orientation as the position on the display were ignored to save transistors. Even with these concessions, the chip had a huge transistor count for the time, but thanks to Moore’s Law the transistor count grew allowing for much nicer to program graphics chips in later consoles and computers.

The TIA had 128 colors that were specified using a 4 bit hue and a 3 bit luminance. This is quite a bit different than the RGB format modern graphics uses. The palette was tied to the televisions of the time so I am talking about NTSC televisions. Oversea versions of the console had different palettes for PAL and for SECAM televisions but for now I am only worrying about the NTSC version of the chip. Each line could have 4 colors which consisted of the background color, the play-field color, the player 0 color, and the player 1 color. These colors could be changed in the middle of drawing a scan-line but with only 76 cycles and a STA taking at least 3 cycles, the number of colors on a line was drastically limited.

The play-field is a 20 bit image that was spread across half of the screen (4 pixels per play-field bit) and could then be repeated or flipped for the other half of the screen. The data making up the 20 bits could be changed allowing for a 40 bit non-symmetric  play-field.

In addition to the play-field, the programmer had 5 sprites to work with. These sprites were intended for specific uses so you had two player sprites, two missile sprites, and a ball sprite. The missile and ball sprites were simply lines that could be 1,2,4, or 8 pixels wide.  The two player sprites were much more flexible allowing for a byte representation of the image so you had 8 pixels to play with. As you could also scale or repeat the byte it was possible with some trickery to have the sprite repeat itself 3 times with the data for the sprite changed between repeats allowing for 6 player sprites to appear on a scan line.

Because each scan line is separate, the height of the sprites is up to the programmer but is effectively the height of the screen with the programmer only enabling the sprite for the rows of the display he or she wants the sprite to appear on.

My implementation plan (I am writing this before I started working on it but am posting this much later) is to first get the colors working, then get the play-field working, then get the sprites working at which point I start synchronizing the TIA emulator with the 6502 emulator and have a working machine less input and sound. Once I have a working display, the input system will be worked on and finally some sound.