Wednesday, August 20, 2014

Atari craziness

So between making more maps for Robo-Ninja (I finished the rest of the Temple zone yesterday!), I'm still playing with my side-SIDE project of learning Atari programming.

Let me just tell you, that machine is insane.

On most modern hardware, drawing game graphics is a matter of setting up a scene in openGL, (or  often, some framework that abstracts away the openGL and lets you tell it what stuff to draw where).

On the GBA, it was a little harder. You had magical memory registers for video ram, which you'd fill up with appropriate graphics, you'd poke palette data into a different register, then you'd have other  registers that told it how to build and display sprites or background tiles based on all that. You could, for example, fill in a register to say "put a 16x16 sprite, flipped horizontally, at position X,Y, based on graphic data at video ram location FOO" You still had to do a good bit of work figuring out how to swap data in and out of the limited video ram, how to update slices of the background tile data to make it look like the screen was scrolling, etc, but it wasn't too bad.

On the Atari, you have to hold the thing's hand THROUGH EVERY TV SCANLINE.  To position a sprite horizontally, you literally have to wait until the scanline is at the right place and say "DRAW IT NOW!" Which is tricky because the only way to track how far you are into the scanline is to count the processor cycles of every assembly command, and know that the scanline advances X amount for every cycle. You only get 76 clock cycles per scanline, which is only around 30 assembly instructions, so you're counting and squeezing to try to minimize every assembly command you send to it while drawing the screen.

Luckily it remembers the horizontal position of where you last said "DRAW IT NOW" so on other scanlines you can focus on doing such trivial tasks as loading each scanline's sprite data into the right register, hoping that you can load it into the register before the scanline passes the sprite's X position, because if you take too long, it will have drawn the sprite before you managed to load the new data.

Oh, and you only get 2 sprites and 120 bytes of ram to work with.

It's horrible and wonderful all at the same time. For some crazy reason, I'm loving it.  Tonight, for example, I was trying to figure out how to prevent the black bars from showing up on the left when you move a sprite mid-frame. Take a look at missile command and frogger:

Those are classic examples of games with positioning lines on the left -- the Atari hardware had a weird quirk where if you moved a sprite's horizontal position partway through the frame, it would draw a black line at the left. There didn't seem to much you could do about it. Quite a few games just gave up and had them. A lot of others (particularly Activision games, or so I've read), made the whole left bar of the screen be black, just to hide them.

But somewhere along the line, some smart person realized you could trick the Atari hardware into not showing them, if you wrote to the move register at EXACTLY the 74th clock cycle, at the end of the scanline. So I spent part of my evening counting clock cycles to try to write to that register at exactly the right time.

I ended up going with an easier but less elegant solution, which was to basically not put any enemies or background changes on that scanline, so I could spend an entire line just burning cycles to get to 74. It will work for what I'm going for, but prevents anything interesting from happening on that line.

Just for kicks, here's the code for managing that scanline:
      STA WSYNC   
        LDA #$80            ;2 prevent other things from moving on early hmove,
        STA HMP0     ;3  so clear out the movement registers 
        STA HMM0     ;3
        STA HMM1     ;3
        STA HMBL     ;3 
        LDY #7              ;2 (16 so far) ... 58 cycles left

        NOP                  ;2  (7 cycles through each iteration of  
                             ;     the burn loop, 8 the last time through,
        DEY                  ;2    because the BNE only takes 2 cycles 
                             ;     if the branch isn't followed)

        BNE BurnScanlineLoop ;3 (2) (burns 48 during loop) ... 10 left

        LDA Temp             ;3  (basically a NOP, but needed to burn
                             ;3 cycles instead of 2)
        NOP                  ;2
        DEX                  ;2  Reduce the line counter since 
                             ;   skipping the regular WSYNC
        STA HMOVE            ;3  Do the MOVE!
        JMP ScanLoopSkipWsync ; Skip the Wsync since we used the whole scanline

And there we have it, folks. Clearly I've descended into unbridled insanity.

No comments: