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.
No comments:
Post a Comment