;
; How to Draw A Playfield.
; by Nick Bensema  9:23PM  3/2/97
;
; Atari 2600 programming is different from any other kind of programming
; in many ways.  Just one of these ways is the flow of the program.
;
; Since the CPU must hold tha TIA's hand through all graphical operations,
; the flow ought to go like this:
;
; Clear memory and registers
; Set up variables
; Loop:
;    Do the vertical blank
;    Do game calculations
;    Draw screen
;    Do more calculations during overscan
;    Wait for next vertical blank
; End Loop.
;
; What I will do is create an outline, and explain everything I can.
; This program will display "HELLO" and scroll it down the screen.
;
; In writing this program, I will take the opportunity to show you
; how a few simple modifications can completely change a program's
; appearance or behavior.  I will invite you to comment out a few
; lines of code, and alter others, so that you can observe the results.
;
; I will be using DASM for now.  Conversion to A65 should be trivial.
;
        processor 6502
        include vcs.h

        org $F000

Temp       = $80
PlayfieldY = $90

Start
;
; The 2600 powers up in a completely random state, except for the PC which
; is set to the location at $FFC.  Therefore the first thing we must do is
; to set everything up inside the 6502.
;
        SEI  ; Disable interrupts, if there are any.
        CLD  ; Clear BCD math bit.
;
; You may feel the need to use the stack, in which case:
;
        LDX  #$FF
        TXS  ; Set stack to beginning.
;
; Now the memory and TIA registers must be cleared.  You may feel the
; need to write a subroutine that only clears out a certain section of
; memory, but for now a simple loop will suffice.
;
; Since X is already loaded to 0xFF, our task becomes simply to ocunt
; everything off.
;
        LDA #0
B1      STA 0,X
        DEX
        BNE B1
;
; The above routine does not clear location 0, which is VSYNC.  We will
; take care of that later.
;
; At this point in the code we would set up things like the data
; direction registers for the joysticks and such.
;
        JSR  GameInit
;
; Here is a representation of our program flow.
;
MainLoop
        JSR  VerticalBlank ;Execute the vertical blank.
        JSR  CheckSwitches ;Check console switches.
        JSR  GameCalc      ;Do calculations during Vblank
        JSR  DrawScreen    ;Draw the screen
        JSR  OverScan      ;Do more calculations during overscan
        JMP  MainLoop      ;Continue forever.
;
; It is important to maintain a stable screen, and this routine
; does some important and mysterious things.  Actually, the only
; mysterious part is VSYNC.  All VBLANK does is blank the TIA's
; output so that no graphics are drawn; otherwise the screen
; scans normally.  It is VSYNC which tells the TV to pack its
; bags and move to the other corner of the screen.
;
; Fortunately, my program sets VBLANK at the beginning of the
; overscan period, which usually precedes this subroutine, so
; it is not changed here.
;
VerticalBlank
        LDX  #0
        LDA  #2
        STA  WSYNC
        STA  WSYNC
        STA  WSYNC
        STA  VSYNC ;Begin vertical sync.
        STA  WSYNC ; First line of VSYNC
        STA  WSYNC ; Second line of VSYNC.
;
; But before we finish off the third line of VSYNC, why don't we
; use this time to set the timer?  This will save us a few cycles
; which would be more useful in the overscan area.
;
; To insure that we begin to draw the screen at the proper time,
; we must set the timer to go off just slightly before the end of
; the vertical blank space, so that we can WSYNC up to the ACTUAL
; end of the vertical blank space.  Of course, the scanline we're
; going to omit is the same scanline we were about to waste VSYNCing,
; so it all evens out.
;
; Atari says we have to have 37 scanlines of VBLANK time.  Since
; each scanline uses 76 cycles, that makes 37*76=2888 cycles.
; We must also subtract the five cycles it will take to set the
; timer, and the three cycles it will take to STA WSYNC to the next
; line.  Plus the checking loop is only accurate to six cycles, making
; a total of fourteen cycles we have to waste.  2888-14=2876.
;
; We almost always use TIM64T for this, since the math just won't
; work out with the other intervals.  2880/64=44.something.  It
; doesn't matter what that something is, we have to round DOWN.
;
        LDA  #44
        STA  TIM64T
;
; And now's as good a time as any to clear the collision latches.
;
        LDA #0
        STA CXCLR
;
; Now we can end the VSYNC period.
;
        STA  WSYNC ; Third line of VSYNC.
        STA  VSYNC ; (0)
;
; At this point in time the screen is scanning normally, but
; the TIA's output is suppressed.  It will begin again once
; 0 is written back into VBLANK.
;
        RTS
;
; Checking the game switches is relatively simple.  Theoretically,
; some of it could be slipped between WSYNCs in the VBlank code.
; But we're going for clarity here.
;
; It just so happens that I'm not going to check any game switches
; here.  I'm just going to set up the colors, without even checking
; the B&W switch!  HA!
;
CheckSwitches
       LDA #0
       STA COLUBK  ; Background will be black.
       RTS
;
; Minimal game calculations, just to get the ball rolling.
;
GameCalc
        INC PlayfieldY   ;Inch up the playfield
        RTS

;
; This is the scariest thing I've done all month.
;
DrawScreen
        LDA INTIM
        BNE DrawScreen ; Whew!
        STA WSYNC
        STA VBLANK  ;End the VBLANK period with a zero.
;
; Now we can do what we need to do.  What sort of playfield do
; we want to show?  A doubled playfield will work better than
; anything if we either want a side scroller (which involves some
; tricky bit shifting, usually) or an asymmetrical playfield (which
; we're not doing yet).  A mirrored playfield is usually best for
; vertical scrollers.  With some creativity, you can use both in your
; game.
;
; The "score" bit is useful for drawing scores with playfield graphics
; as Combat and other early games do.  It can also create a neat effect
; if you know how to be creative with it.  One useful application of
; score mode would be always having player 1 on the left side, and
; player 0 on the right side.  Each player would be surrounded in the
; opposite color, and the ball graphic could be used to stand out
; against either one.  On my 2600jr, color from the right side bleeds
; about one pixel into the left side, so don't think it's perfect.
; It's really far from perfect because PC Atari does not implement
; it at all; both sides appear as Player 0's color.  A26 does, though.
;
; To accomodate this, my routine puts color values into
; COLUP0 for the left side, and COLUP1 for the right side.  Change
; the LDA operand to 0 or 1 to use the normal system.  The code in
; the scanning loop accounts for both systems.
;
        LDA  #2
        STA  CTRLPF
;
; Initialize some display variables.
;
        ;There aren't any display variables!
;
; I'm going to use the Y register to count scanlines this time.
; Realize that I am forfeiting the use of the Y register for this
; purpose, but DEC Zero Page takes five cycles as opposed to DEY's
; two, and LDA Zero Page takes three cycles as opposed to TYA's two.
;
; I'm using all 191 remaining scanlines after the WSYNC.  If you
; want less, or more, change the LDY line.
;
; This is a decremental loop, that is, Y starts at 191 and continues
; down to 0, as do all functions of Y such as memory locations, which
; is why the graphics at the end of this file are stored "bottom-up".
; In a way, one could say that's how the screen is drawn.  To turn this
; into an incremental loop, change the number to 255-191 (65) and change
; the DEY at the end ot the scanning loop to INY.
;
        LDY #191
;
; Okay, now THIS is scary.  I decided to put the bulk of my comments
; BEFORE the code, rather than inside it, so that you can look at the
; code all at once.
;
; Notice the new method of cycle counting I use.  I'll send an update
; to cyccount.txt RSN.
;
; This routine came out surprisingly clean.  There are no branches,
; and most updates are made even before PF0 becomes visible at cycle 23,
; even though PF1 and PF2 don't become visible until, by my estimate,
; cycles 29 and 40, respectively.  We could use this time to get player
; shape and colors from temp variables and sneak them in, but that's
; another file.  In fact, at the last minute I re-arranged things
; and threw in some color changes.
;
; The playfield will only be moved up every 4 scanlines, so it doesn't look
; squished.  I could have updated it every 2 scanlines, and that would have
; saved two cycles. I could have saved another two cycles by having it
; change EVERY scanline.  Comment out one or both of the ASL's to see what
; this would look like.  I realize that it updates the PF registers whether
; it needs it or not, but it would be pointless to branch around these
; updates.  Better to know you're wasting cycles and have them counted
; than to get unlucky and have your code spill into the next scanline
; every time too many things get updated.
;
; This is going to be a moving playfield.  For a stationary playfield,
; comment out the SEC and SBC lines.  That's probably what most of you all
; are going to want, anyway.  And for a really good moving playfield,
; like in River Raid or Vanguard, you'll need slightly more interesting
; code than I'm prepared to provide.
;
; I also could have made the playfield graphic 16 bytes high, or 32, or 64,
; by changing only my data and the AND line.  AND can serve as a modulus
; for any power of two (2^n) up to 128, by ANDing a byte with that number
; minus one ( (2^n)-1 ).  8 bytes * every 4 scanlines == 32, which is
; a power of two, which is why this works.  Try commenting out the AND line
; and see how the program interprets it.  Remember that PlayfieldY goes
; up to 255.
;
; But you won't need to worry about that if you're drawing a stationary
; playfield where the graphics data is so large, it doesn't need to repeat.
; In that case, you don't need the AND line and you don't need to make sure
; your graphics are 2^n bytes tall.  Comment out the AND, SEC and SBC lines,
; and add a third LSR to the block of two.  It indexes a bit too far at the
; top of the screen, which explains the garbage.  You can fix that problem
; either by adding more data to the end of each array, or by decreasing
; the resolution by adding a fourth or fifth LSR.
;
; And who's to say you'll need all three playfield registers?  Perhaps
; you have a rather narrow playfield, or one that's always clear in the
; middle.  Either choice will save you five cycles per scanline.
;
; As you can see, it can be trimmed down quite a bit, and I still have
; a lot of cycles left over.  The maximum, if you recall, is 73 if you
; plan to use STA WSYNC, and I pity the fool who doesn't.

ScanLoop
; Result of the following math is:
;  X = ( (Y-PlayfieldY) /4 ) mod 7
        TYA
        SEC
        SBC PlayfieldY
        LSR   ;Divide by 4
        LSR
        AND #7  ;modulo 8
        TAX
        LDA PFData0,X           ;Load ahead of time.
; WSYNC is placed BEFORE all of this action takes place.
        STA WSYNC
        STA PF0                 ;[0] +3 = *3*   < 23
        LDA PFLColor,X          ;[3] +4
        ;In a real game, I wouldn't be this redundant.
        STA COLUP0              ;[7] +3 = *10*  < 23
        STA COLUPF              ;[10]+3 = *13*  < 23
        LDA PFData1,X           ;[13]+4
        STA PF1                 ;[17]+3 = *20*  < 29
        LDA PFRColor,X          ;[20]+4
        STA COLUP1              ;[24]+3 = *27*  < 49
        LDA PFData2,X           ;[27]+4
        STA PF2                 ;[31]+3 = *34*  < 40
        DEY
        BNE ScanLoop
;
; Clear all registers here to prevent any possible bleeding.
;
        LDA #2
        STA WSYNC  ;Finish this scanline.
        STA VBLANK ; Make TIA output invisible,
        ; Now we need to worry about it bleeding when we turn
        ; the TIA output back on.
        ; Y is still zero.
        STY PF0
        STY PF1
        STY PF1
        STY GRP0
        STY GRP1
        STY ENAM0
        STY ENAM1
        STY ENABL
        RTS

;
; For the Overscan routine, one might take the time to process such
; things as collisions.  I, however, would rather waste a bunch of
; scanlines, since I haven't drawn any players yet.
;
OverScan   ;We've got 30 scanlines to kill.
        LDX #30
KillLines
         STA WSYNC
         DEX
         BNE KillLines
        RTS

;
; GameInit could conceivably be called when the Select key is pressed,
; or some other event.
;
GameInit
        LDA #0
        STA PlayfieldY
        RTS

;
; Graphics are placed so that the extra cycle in the PFData,X indexes
; is NEVER taken, by making sure it never has to index across a page
; boundary.  This way our cycle count holds true.
;

        org $FF00
;
; This is the tricky part of drawing a playfield: actually
; drawing it.  Well, the display routine and all that binary
; math was a bit tricky, too, but still, listen up.
;
; Playfield data isn't stored the way most bitmaps are, even
; one-dimensional bitmaps.  We will use the left side of the
; screen only, knowing that the right side is either repeated
; or reflected from it.
;
; In PF0 and PF2, the most significant bit (bit 7) is on the RIGHT
; side.  In PF1, the most significant bit is on the LEFT side.  This
; means that relative to PF0 and PF2, PF1 has a reversed bit order.
; It's just really weird.
;
;    PF0  |     PF1       |      PF2
;  4 5 6 7|7 6 5 4 3 2 1 0|0 1 2 3 4 5 6 7
;
; This is important to remember when doing calculations on bytes intended
; for the PF registers.  Defender gives a good example of this.
;
; It will become necessary to write a program that makes this easier,
; because it is easy to become confused when dealing with this system.
;
PFData0  ;H       4 5 6 7
       .byte $00,$f0,$00,$A0,$A0,$E0,$A0,$A0
PFData1  ;EL      7 6 5 4 3 2 1 0
       .byte $00,$FF,$00,$77,$44,$64,$44,$74
PFData2  ;LO      0 1 2 3 4 5 6 7
       .byte $00,$FF,$00,$EE,$A2,$A2,$A2,$E2
PFLColor ; Left side of screen
       .byte $00,$FF,$00,$22,$26,$2A,$2C,$2E
PFRColor ; Right side of screen
       .byte $00,$1F,$00,$6E,$6C,$6A,$66,$62

        org $FFFC
        .word Start
        .word Start