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!")
  }
}

Sunday, July 14, 2019

Accessing the TIA

This was suppose to be posted last Saturday but I got messed up and posted an entry to my Blazing Games Development Blog instead.  Then I had an anniversary party to attend last night and forgot to post this before leaving. To get things back on the proper schedule, I will post the next installment next week instead of next fortnight. It should be noted that once the TIA rendering material is posted I may be slowing down to monthly as I really don't have much time for work on my emulation project.

 We are at the point in the project where we want to display graphics on the screen. We also want to write code so that it will work on the JVM as well as on the browser and possibly native code. The Kotlin language supports exactly this by using the “expect” and “actually” keywords. This works by letting you set up classes and functions with the expect keyword that will be implemented in a platform dependent way. Each platform that you are supporting would then have an implementation of the classes and functions using the actual keyword. This allows you to have generic code be able to call platform specific code and makes it easy to add new platforms by simply writing the expect classes and functions for that platform.

Unfortunately, I started using the IntelliJ IDE for my Kotlin development and set up the project before they added support for multiplatform projects. While I probably could create a new project as a multi-platform project and then try to pull my repository into that project, I suspect that would lead to a huge mess which would waste a lot of my time. Instead my plan going forward is to just do a JavaFX version and later port to JavaScript once I have everything working. 

One thing that happens when you are working on a project is that your initial ideas about how things will work will often have missing or incorrect assumptions. This is one reason why the waterfall method of development fails so often. As you work on a project you gain a better understanding of the problems involved. One assumption that I had was that the TIA chip could simply read the memory to perform its rendering. Unfortunately, after reviewing the TIA documentation, it seems that several registers work by being written to, even if the value written is the same value in memory. 

To solve this issue, I am going to have to rework the way memory is handled. What I am planning on doing is having both the A2600 class and the cartridge be memory managers with the A2600 class being the one that the m6502 instance uses. That way when memory is written or read from within the TIA register range, the TIA gets called so it can perform register operations. 

Internally, I could represent the registers with a block of memory, but we still need to track trigger writes. As we are having methods to handle reading and writing it is probably easier to have a different internal representation to allow for easier rendering by having classes for display objects which will greatly simplify the emulation. 

Having raw numbers is not useful, so I created an enumeration of the registers and their address. The problem here is that when I am calling the functions, the register is specified as an address so to use the enumeration you need to access the address component of the enumeration. A better approach would be to have static class constants. The problem is that Kotlin doesn’t support them. Companion objects could be used to perform a similar function but they are very inefficient.

This got me thinking that as the enum is just a class, why not use a singleton class in it’s place. Kotlin has a built-in singleton class type which is declared using object instead of class as there is only one instance.  This means that any const val declared in the object will be static constants. So I set up a TIAPIARegs object that holds all the TIA as well as the PIA registers. I have not yet decided if I will combine the TIA and PIA emulators together or create a separate class. I am using the constants from the Stella documentation which I found on the internet archives (\url{https://archive.org/details/StellaProgrammersGuide}).

@Suppress("unused")
object TIAPIARegs {
const val VSYNC  = 0x00     // vertical sync - set to 2 to start sync and to 0 to stop
const val VBLANK = 0x01     // vertical blank
const val WSYNC  = 0x02     // wait for sync
const val RSYNC  = 0x03     // Reset sync - for testing
const val NUSIZ0 = 0x04     // number-size player-missile 0  -> xxMMxPPP =Missile size, Player size/copies)
const val NUSIZ1 = 0x05     // number-size player-missile 1  -> xxMMxPPP =Missile size, Player size/copies)
const val COLUP0 = 0x06     // color-luminosity player 0
const val COLUP1 = 0x07     // color-luminosity player 2
const val COLUPF = 0x08     // color-luminosity playfield
const val COLUBK = 0x09     // color-luminosity background
const val CTRLPF = 0x0A     // control playfield ball size & collisions
const val REFP0  = 0x0B     // reflect player 0
const val REFP1  = 0x0C     // reflect player 1
const val PF0    = 0x0D     // Playfield register 0 =upper nibble display order 4..7)
const val PF1    = 0x0E     // Playfield register 1 =display order 7..0)
const val PF2    = 0x0F     // Playfield register 2 =display order 0..7)
const val RESP0  = 0x10     // reset player 0
const val RESP1  = 0x11     // reset player 1
const val RESM0  = 0x12     // reset missile 0
const val RESM1  = 0x13     // reset missile 1
const val RESBL  = 0x14     // reset Ball
const val AUDC0  = 0x15     // audio control 0
const val AUDC1  = 0x16     // audio control 1
const val AUDF0  = 0x17     // audio frequency 0
const val AUDF1  = 0x18     // audio frequency 1
const val AUDV0  = 0x19     // audio volume 0
const val AUDV1  = 0x1A     // audio volume 1
const val GRP0   = 0x1B     // graphics player 0 =bit pattern)
const val GRP1   = 0x1C     // graphics player 1
const val ENAM0  = 0x1D     // graphics =enable) missile 0
const val ENAM1  = 0x1E     // graphics =enable) missile 1
const val ENABL  = 0x1F     // graphics =enable) ball
const val HMP0   = 0x20     // horizontal motion player 0
const val HMP1   = 0x21     // horizontal motion player 1
const val HMM0   = 0x22     // horizontal motion missile 0
const val HMM1   = 0x23     // horizontal motion missile 1
const val HMBL   = 0x24     // horizontal motion ball
const val VDELP0 = 0x25     // vertical delay player 0
const val VDELP1 = 0x26     // vertical delay player 1
const val VDELBL = 0x27     // vertical delay ball
const val RESMP0 = 0x28     // Reset missile 0 to player 0
const val RESMP1 = 0x29     // Reset missile 1 to player 1
const val HMOVE  = 0x2A     // Apply horizontal motion
const val HMCLR  = 0x2B     // clear horizontal motion registers
const val CXCLR  = 0x2C     // clear collision latches

// Read registers
const val CXM0P  = 0x30     // read collision =bit 7) MO with P1 =Bit 6) M0 with P0
const val CXM1P  = 0x31     // read collision =bit 7) M1 with P0 =Bit 6) M1 with P1
const val CXP0FB = 0x32     // read collision =bit 7) PO with PF =Bit 6) P0 with BL
const val CXP1FB = 0x33     // read collision =bit 7) P1 with PF =Bit 6) P1 with BL
const val CXM0FB = 0x34     // read collision =bit 7) MO with PF =Bit 6) M0 with BL
const val CXM1FB = 0x35     // read collision =bit 7) M1 with PF =Bit 6) M1 with BL
const val CXBLPF = 0x36     // read collision =bit 7) BL with PF =Bit 6) unused
const val CXPPMM = 0x37     // read collision =bit 7) P0 with P1 =Bit 6) M0 with M1
const val INPT0  = 0x38     // Dumped =Paddle) input port 0
const val INPT1  = 0x39     // Dumped =Paddle) input port 1
const val INPT2  = 0x3A     // Dumped =Paddle) input port 2
const val INPT3  = 0x3B     // Dumped =Paddle) input port 3
const val INPT4  = 0x3C     // Latched =Joystick) input port 4
const val INPT5  = 0x3D     // Latched =Joystick) input port 5

const val SWCHA  = 0x280
const val SWACNT = 0x281
const val SWCHB  = 0x282
const val SWBCNT = 0x283
const val INTIM  = 0x284
const val TIMINT = 0x285

const val TIM1T  = 0x294
const val TIM8T  = 0x295
const val TIM64T = 0x296
const val T1024T = 0x297

const val TIM1I  = 0x29c
const val TIM8I  = 0x29d
const val TIM64I = 0x29e
const val T1024I = 0x29f

// Player sprite scale/copy modes
const val PMG_NORMAL = 0
const val PMG_TWO_CLOSE = 1
const val PMG_TWO_MEDIUM = 2
const val PMG_THREE_CLOSE = 3
const val PMG_TWO_WIDE = 4
const val PMG_DOUBLE_SIZE = 5
const val PMG_THREE_MEDIUM = 6
const val PMG_QUAD_PLAYER = 7

// Internal collision bits
const val ICOL_PFBL = 1
const val ICOL_PFP0 = 2
const val ICOL_PFM0 = 8
const val ICOL_PFP1 = 64
const val ICOL_PFM1 = 1024
const val ICOL_BLP0 = 4
const val ICOL_BLM0 = 16
const val ICOL_BLP1 = 128
const val ICOL_BLM1 = 2048
const val ICOL_P0M0 = 32
const val ICOL_P0P1 = 256
const val ICOL_P0M1 = 4096
const val ICOL_M0P1 = 512
const val ICOL_M0M1 = 8192
const val ICOL_P1M1 = 16384

// internal sprite indexes
const val ISPRITE_BALL = 0
const val ISPRITE_PLAYER1 = 1
const val ISPRITE_MISSILE1 = 2
const val ISPRITE_PLAYER0 = 3
const val ISPRITE_MISSILE0 = 4
}

The registers are assigned 5 to 6 character labels which are condensed descriptions of what the register does. After working with the 2600, the labels do start to make sense. The reason for the condensed names was likely due to limitations of the assemblers of the time. It was not unheard of for labels to be very limited in length, with eight characters being a common limit. 

We will be covering the registers in large groups as the respective functionality of the TIA is implemented. Our first task, however, is to get the color clock working.