skip navigation

Atari 2600 Programming: Structuring Graphics Data in 6507 Assembly

Description

In this episode, cover several ways to store and organize with graphic binary data for you atari 2600 games.

Released:
September 8, 2025

Original Link:
Atari 2600 Programming: Structuring Graphics Data in 6507 Assembly

Transcription

Your support is greatly appreciated! https://www.patreon.com/8blit

S06E01 Data Structures

Welcome back to 8Blit! In this episode, we’re tackling the art of structuring graphic data on the Atari 2600. With only 4KB of ROM, a mere 128 bytes of RAM, and exactly 76 machine cycles per scanline, every single byte has to earn its keep. To make a great game, we have to stop thinking like artists and start thinking like engineers. … or vice versa. It swings both ways. ac/dc? We’ll build from basic player graphics to multi-frame animations, zero-page buffering, and byte streams. By the end of this episode, you’ll have the toolkit to structure your own games for peak performance - or at the very least, you’ll have the skills to reverse-engineer or hack the graphics of your favorite classic titles. Let’s get to work.

Before we dive into the code, I just want to say a huge thank you to my Patrons. Keeping all my project files and tutorials completely free for everyone is really important to me, and it’s only possible because of their support. If you’d like to help keep the channel going and support more free content like this, check out the link in the description! Now let’s get back to the good stuff.

Why Data Structures Matter on the 2600: A Quick History and Overview Upon its release in 1977, Atari VCS games were limited to a meager 128 bytes of RAM and either 2-kilobyte or 4-kilobyte ROM chips. These days, we rarely even mention megabytes; we usually describe sizes in gigabytes for RAM and terabytes for drives—with drives being one of the closest historical analogues to a ROM chip. Feel free to argue with me in the episode comments that CD, DVD, and BLURAY are the more accurate comparison. It’s hard to conceptualize the sheer difference in data volume between what’s commonplace today and what was groundbreaking in 1977.

The MOS 6532 RIOT chip provided the Atari VCS with those 128 bytes of RAM that programmers used to track your score, your plane’s position, or how much gas you had left. Often, due to the very limited registers available on the MOS 6507 processor, you might even need some of those bytes just to store temporary variables used in your calculations. The top of RAM is reserved for the call stack, which automatically saves your return address whenever you jump to a subroutine, so the CPU knows where to continue after it returns. Each nested subroutine call pushes 2 bytes onto the stack, and if nesting runs too deep, the stack can grow downward into your variable RAM, corrupting your data.

ROM must also hold all your game’s graphics, sounds, and lookup tables alongside your code. As mentioned, the maximum ROM size for early games was very small. In fact, due to cost-cutting in the console’s design, the addressable space was capped at 8KB total, leaving only 4KB for cartridge ROM. It was later-on that developers and chipmakers figured out a technique called bank switching, which allowed them to switch between different 4-kilobyte segments of larger ROM chips, like 8, 16, or even 32 kilobytes. These larger ROMs added to the cost of manufacturing game cartridges, so many times the scope of a game would be scaled down to fit within the standard 4k. Bank switching is an advanced technique that we’ll cover in a later episode, so be sure to hit the subscribe button to be notified of new videos.

Even with the larger sizes eventually available to developers, it’s still vitally important to structure your data in a way that your code can easily read and process. With the limited speed of the MOS 6507 processor, you only have 76 machine cycles to draw your graphics. You’re literally racing the electron beam in the television, and you need to find a way to set up your graphics and write to the registers before the beam passes that part of the screen… and you need to do this all over again for every scanline. For more information on scanlines, machine cycle counting, and timing, check out our episode on asymmetrical playfields. It covers this topic in great detail.

Sometimes just restructuring your graphics a little differently gives you a modest boost in efficiency - and that’s exactly why we’re going to look at the top-down versus bottom-up approaches. Other times it makes sense to spend a few extra machine cycles, but we can cleverly offload that work to the VBLANK or overscan periods where the CPU is free. Once we’ve covered those ideas, I’ll walk you through a more advanced trick that takes full advantage of zero-page RAM for fast reads. And finally, I’ll show you a simple Google Sheets tool I made to help me draw and structure my graphic data into bytestreams so I don’t need to hand-enter 1’s and 0’s constantly. And… with that foundation laid, let’s start with the basics.

Stable Screen Kernel and Basic Player Graphics Before we start talking about data structures and looking at example code, I want to mention that our first example is a bare template that all following examples will build upon. All it does is create a stable screen. It generates 262 scanlines, which is the standard for NTSC. This is broken down into 3 lines for vertical sync, 37 for the vertical blank, 192 for video-out, and the remaining 30 for overscan. For more information on generating a stable screen, please refer to our episode titled “Your First Code for the Atari 2600” where we break down the different areas of the screen and explain what the code is doing. A link is provided in the description.

In this example, we’re going to discuss the differences between top-down and bottom-up graphics processing. Generally, when you think about drawing a graphic, you picture it just as you see it on the screen, with the character’s head at the top and their feet at the bottom. Certainly, this is how we want the final image to look, but depending on your use case, it might be better to store the bitmap upside down in your ROM, with the feet at the top and the head at the bottom. It all depends on how you want to draw your entire screen. Typically, you use a large portion of the screen, but your graphic might only be ten scanlines tall. As you move down the screen one scanline at a time, you need to know exactly which scanline to start drawing and on which one to stop. This requires calculating whether your graphic is visible on every single scanline. If it is, you grab the correct line of the bitmap; if not, you set the data to zero so nothing is drawn.

Furthermore, you need to calculate whether you should even be drawing a scanline at all, such as checking if you’ve reached the bottom of the video-out area. That results in two calculations you have to make on every scanline, regardless of whether you’re actually drawing an object. To save time, developers often count down for each scanline, starting at 192 and decreasing to zero, effectively flipping the Y-coordinate system. They don’t do this to save ROM space or because the graphic loads faster. It’s done simply because it is faster to count down to zero than it is to count up to 192.

This is due to how instructions work on the 6507 processor. If I’m counting up and want to know if I’ve reached scanline 192, I have to increment the index register, then use a “Compare” instruction to check the register against the value 192. If they match, the processor sets a zero status flag, and I then use a branch instruction that checks for the zero flag to act on that result. However, if I count down to zero, I have a distinct advantage. The “Decrement” instruction automatically sets the zero flag if the result is zero. This means we can skip the “Compare” instruction entirely and jump straight to the branch instruction. This saves two machine cycles. If we apply that same logic to our graphic visibility calculations, we save a total of four machine cycles per scanline. Since we only have 76 machine cycles per scanline to begin with, that’s a significant gain. The downside is that you have to define your graphics upside down in your code, which can be a little confusing when you’re making small edits. Ultimately, though, this is a great way to reclaim cycles that can be repurposed for more complex tasks.

Sometimes the top-down approach is more beneficial. For example, if you don’t need those extra cycles, there’s no reason to make your code more complex. If you’re splitting the screen into different horizontal bands—say, ten scanlines each—and your graphic is also ten scanlines tall, you don’t need to determine if you should draw on each line because you’re already drawing every line within that band. I’ve used this approach in the new game I’m releasing soon, as well as in the code examples for the episode “Atari 2600 Light Gun Programming.” In that project, I created several bands on the screen where robots run across for you to shoot with the Atari XG-1 light gun. You can find a link to that episode in the description.

In our bottom-up versus top-down example code, we are simply drawing two graphics in a small loop. For one, we are counting an index down, and for the other, we are counting an index up. If we look at the bitmaps, you can see how the same graphic is used for both, with one of them stored inverted. Now that we have our graphics on the screen, let’s give them some life and animate them.

Animating with Pointers: Flexible Frame Switching Sometimes it’s not the graphics data itself that eats up valuable ROM. It’s the code needed to access and animate that data. A classic example is handling animation frames for your player sprite. Imagine you have six animation frames, and each frame is ten bytes tall. Right there you’re using sixty bytes just for the pixel data. Add another six bytes if you want a zero byte at the end of each frame to cleanly clear the player register when the animation is done. That’s already a decent chunk of your four-kilobyte cartridge.

Newer developers often try to invent their own system. They end up with complicated chains of compares and branches to figure out which frame to show, or they do runtime math - taking a base address, multiplying by the frame height, then adding an offset based on the current frame number. Both approaches hard-code a lot of logic and force the 6507 to do extra work every time you need to switch frames. All those comparisons, branches, or multiplications eat cycles. And in a kernel where you have only seventy-six machine cycles per scanline, those extra operations can quickly push you over budget.

A much cleaner solution is to use pointers. With pointers, you create two small tables in ROM to store 16-bit addresses: one for the low bytes of each frame’s starting address, and one for the high bytes. The assembler figures out those addresses for you at build time, so you don’t have to do any of that math yourself during execution. When it’s time to show a particular frame - say frame number three - you simply use that number as an index into the tables. Grab the low byte, store it in zero page. Grab the high byte, store it right after. Now those two consecutive zero-page RAM locations hold a complete sixteen-bit pointer to the frame data.

Inside the kernel you load with indirect indexed addressing, where Y is the current scanline offset within the sprite. That single instruction uses the 16-bit address stored in RAM to pull the correct byte for the chosen frame. No comparison. No multiplication. No branching maze. Just a fast, constant-time lookup. The code stays simple and readable. If you later add more animated objects, the same pointer technique scales beautifully without turning your kernel into a complicated knot of spaghetti. For a deeper dive on how indirect addressing works on the 6507, check out our earlier episode called ‘Animating Graphics on the Atari 2600.’ I’ll drop a link in the description. Pointers are already a big win - they save ROM, simplify maintenance, and keep your kernel cycle-efficient. But we can go one step further, and even faster.

Zero-Page Optimization: Buffering Graphics for Speed In the previous example, we moved away from reading hardcoded memory locations with complicated branching conditions directly inside our video-out kernel. Instead, we switched to using pointers, handling most of the processing during assembly time, vblank, or overscan. This lets us easily and consistently read the correct byte from the correct animation frame. This example builds on that approach and trades some of our zeropage RAM for even greater efficiency in the video-out kernel.

In the last example we used Load Accumulator indirect - indexed mode. This allowed us to create flexible “pointers” to our graphic data. We’re going to keep using pointers, but instead of performing the indirect load during the video-out loop, we’ll do the work earlier, in vblank, and copy the entire sprite frame into zeropage RAM ahead of time. Why do this? It comes down to how addressing modes and instruction timing work on the 6507. When using Load Accumulator’s indirect - indexed mode the instruction takes 5 cycles (or 6 cycles if a page boundary is crossed). This is because it needs to do a redirect to the address you specified, and if the address is located in ROM, it needs to use a 2-byte, 16-bit address. Loading directly from zero page RAM is much faster. Zero page only needs a 1-byte address instead of a full 2-byte address, so the CPU fetches and processes the instruction in fewer clock cycles Here’s how all of this comes together in code.

We first need to reserve some zero page ram for our player graphics. Here we’re defining two variables. One for player 0, and the other for player 1. Each variable is reserving 11 bytes to hold a single frame of animation. Below that are the two pointers. One each for our player graphics. These are both 2 bytes, because the location of our graphic animation frames are stored in ROM which requires a 16-bit address to access. As we did in the previous example, we’ll build our data tables with the addresses to each of the animation frames. Now we need to use our data tables to assemble the 16-bit address to the current animation frame, then read in the ROM bytes, and write them to the space we reserved for them in zeropage RAM. This subroutine processes the player 0 and player 1 graphics separately because each player graphic is going to use a different animation frame, just to give the movement a little flair. Now that our bytes are safely delivered to zeropage RAM, we no longer need to worry about pointers in our video-out kernel. It’s as simple as read-bytes, write-bytes. Move on to the next.

Through these examples, we’ve progressed from absolute addressing, to indirect - indexed, and now to zeropage loads for the main video-out loop. That’s a significant overall saving in cycles in our video-out kernels. Now let’s shift away from player graphics and look at playfield graphics, exploring a couple of different ways to efficiently load multiple bytes of data per scanline. … but before we do that, I want to ask you to subscribe to the channel if you enjoy learning about Atari 2600 game programming and want to know more about it. Subscribing, giving an episode a like, and letting us know what you liked, or didn’t like about the episode you watched really helps us out, and lets us know how to direct future episodes. Thank you so much for your support. Now, on with the show!

Playfield Graphics by Register: Structured Tables Loading player graphics is child’s play compared to the playfield. It’s the difference between 1 byte for a single player - or 2 bytes for two - versus 3 bytes for a standard playfield, and 6 bytes for an asymmetrical one. Similar timing constraints apply, but the playfield comes with more rules. While the registers are only 3 bytes wide, creating an asymmetrical look means we have to rewrite those registers before the electron beam reaches the halfway point of the screen, then rewrite them again before the beam starts the next line. It’s a constant race. The way these graphics are displayed is also… unique. Both the PF0 and PF2 registers display their bits in reverse order, while PF1 displays them normally. To make it even weirder, PF0 only uses the upper four bits of the byte.

This makes designing and modifying playfields a bit of a headache. To fix that, I use a tool we’re all familiar with: the spreadsheet. Whether you’re an Excel, Google Sheets, or Numbers person, we can use formulas to handle the heavy lifting. This is a template I built in Google Sheets (link in the description if you want to grab a copy). I wanted to be able to “draw” using the cells. I set up a conditional formatting rule: if a cell isn’t empty, the background color changes. Now I can move around with the arrow keys, hit the spacebar to “paint” a pixel, and delete to erase it. I then mapped out exactly which registers the bits belong to: PF0, PF1, and PF2. This sheet is configured for a mirrored-asymmetrical layout. I created six columns that look at my drawing and automatically build the binary strings.

The formulas handle the “spicy” part: they automatically flip the bit order for PF0 and PF2 so I don’t have to think about it. If you’re using mirrored mode, the logic for right side of the screen flips again because the TIA reflects the image. In my specific code, I’m only using PF1 and PF2. I’ve intentionally left PF0 empty on both sides to save ROM space. If you don’t need to draw it, don’t store the data. This leaves me with four bytes of data per line of playfield graphics.

To keep the code efficient, I store the graphics by column rather than by line. I’ve created four data tables: PF1a, PF2a, PF2b, and PF1b. For every scanline, the code reads from its dedicated table. The index is simply the current row of graphics we want to show; we read the value, write it to the register, increment, and repeat. Since I’m counting up, the data tables are organized top-down - head at the top, feet at the bottom. Now, we can just copy these columns and paste them into our assembly code.

But editing raw binary in a text editor is a nightmare. To keep the code clean, the spreadsheet also converts these binary values into hexadecimal. It doesn’t save any ROM space - a byte is a byte whether it’s binary or hex - but it makes the code much more readable. I even added a formula to wrap the hex values into a formatted data table string. Now, instead of manually editing 47 lines of code, I just copy and paste four lines. The code for this example is pretty close to example 2 where we drew normal player graphics from ROM. No pointers, no zeropage RAM. You can check it out for yourself on our GIT Repo, along with all the other examples for every other episode. Be sure to Star the repo to let me know you like it. A link is down in the episode description. For the next step, we’ll evolve this into a compact byte stream for animation.

Advanced Animation: Byte Streams and Large Characters In this example, we’re going to be using several graphics for our animation - five, to be exact. You know, if we stuck with the same method from the last example, we’d end up with four data tables for each of those five graphics, totaling 20 data tables. That’s a pretty heavy load for our code to handle, right? And since we’re dealing with animations, we’d also need to set up pointers to every single one of those tables. Honestly, that’s way too much for me to keep straight in my head. So, to make things easier, we’re going to store each complete graphic in just one table.

Let’s take another look at our spreadsheet, this time switching over to the second tab. Here, you can see each frame of the animation sketched out in the playfield columns. But this time, the sheet is set up for a non-mirrored asymmetrical layout. So, the order here is PF1a, PF2a, PF0b, PF1b. Obviously, that means our formulas are going to be a bit different… but hey, that’s not the only change. In fact, the whole thing looks pretty different now. Sure, the binary columns haven’t changed, and neither have the hex columns, but when it comes to how we’re ordering our graphic data table, it’s like night and day. In the previous example, we just appended the playfield columns from top to bottom—one data table per column. That was straightforward, wasn’t it?

This time, though, we’re going back to that bottom-up approach, where the first data we write gets read from the end of the table, and we work our way to the beginning. As I mentioned before, this saves us some machine cycles per frame, which is always a nice bonus. So, we’re starting to compile the bytes for our stream by grabbing the last row of the graphic first, pulling the values for those playfield columns in the order they’ll be displayed: PF1a, PF2a, PF0b, and PF1b. Then, we just work our way up through the other rows. You can see the stream that our formula generated - the first four bytes match up to the four bytes in that last row, the next four to the second-last row, and so on, and so on. I’m not going to dive deep into the formula itself, because it’s tailored specifically to this way of generating the stream. Your own projects might do things differently, and that’s totally fine. But you’ve got access to the spreadsheet right there in the episode description, so go ahead, make a copy, and tinker with it yourself.

Once I got the formula just right, it was simple - I just copied it and pasted it onto the first line of the other graphics in the animation. Now, to bring it into our code, we can select that column on the sheet, hop over to our code editor, and paste it right in. Before we jump into the code for displaying the graphic, though, we need to set up those pointer tables so we can easily reference each frame of the animation. Just like in our previous animation example, we’re loading the current frame into zero-page RAM to make it quicker and simpler to access. This subroutine grabs the index of the current animation frame, then uses the pointers to copy each byte into RAM… and check out this line in particular…

Graphic_ram is the base address for the graphic we want to draw into the playfield registers… but what’s X all about? Normally, with a player graphic, this index would point to the line -or byte - of the graphic we’re drawing. But here, since we’re working with playfield registers and our graphic uses four bytes per line, it’s a little different. We could decrement X for each byte and write it to the next playfield register, but that would cost us two cycles per decrement… and frankly, it would make the code look a bit cluttered. In this case, X is the offset from the base address to the first of those four bytes for the line we want to draw. We figure out that address by creating a data table of memory offsets.

Here’s how it works: we’ve got our graphic data stored in zero page at the address tied to Graphic_ram. The offsets table is basically a list of zero-page addresses pointing to the start of each graphic line. With that, we can decrement X to grab the address for the line we’re after Now, we can leverage our assembler to make referencing all four bytes of each line a breeze. When the assembler builds our binary ROM, it turns all our variables into hard-coded memory addresses. As we define graphic_ram and reserve the bytes, the assembler figures out the base address in RAM and swaps in any references to graphic_ram with that actual address.

So, our line of code ends up looking like this. And since that address is baked right into the ROM file, we can tweak the code to have the assembler use a different address. Here, for example, we’re telling it to use the base address of graphic_ram plus one. Without us doing any math or burning machine cycles, we’re now reading the second byte of the graphic. And we keep going like that until we have all four bytes. For the next line of the graphic, we just decrement X to get the base offset for the next four bytes. Once you get comfy with how the assembler really operates, you can start using it to handle some of that heavy lifting for you - and it can be a game-changer.

We started off with a stable kernel, then we added static and animated player graphics using pointers and zero-page memory, and finally scaled up to playfield streams—all to keep things optimized within those 76 cycles. Remember: going bottom-up gives us that efficiency boost, pointers add flexibility, zero-page ramps up the speed, and streams keep our animations nice and compact. There’s a link to the Google spreadsheets we used in the examples right there in the episode description. When you click on it, it’ll prompt you to automatically create a copy for yourself on your own Google Drive. So, what have you been using to design your graphics? Go ahead and share in the comments below!

If you enjoyed this, give it a like, subscribe, and hit that bell for notifications. Join our Discord for more discussions, and check out our merch store—links are all in the episode description. As always, all the example code for this episode is the github repo; just follow the link in the episode description. Have you programmed Atari today? That’s all for now - thanks for watching, and I’ll catch you later!

Back to top of page