Tiles in Assembly I - Basics
When discussing graphics in game programming we typically consider sprites and tiles. In the 8 bit era very few games were written in bitmap mode (except on systems like Sinclair Spectrum) so tile graphic modes and manipulation of tiles is critical in writing games.
On the Commander X16 VERA is responsible for all the graphics and more so we will use some of the same techniques we learned when we were working on sprites. We will be reading and writing to VERA registers, use Palette and transfer data from CPU RAM to Video RAM (VRAM).
I recommend having VERA Registers reference Sheet Handy for any kind of graphics work.
Let’s start with some definitions:
Display resolution
Commander X16 always outputs images at 640 x 480 pixels at 60 hertz. Internally however we can scale the display to lower resolutions as long as we stay in the power of 2 range. For example, the common Display resolution for games is 320 x 240 pixels. In this case we set up VERA with horizontal and vertical scale both at 2x. 4x Scale gives us 160 x 120 pixels and so on. Of course we can control that individually with e.g. 1x scale horizontally and 4x scale vertically for 640 x 120 pixels.
It is important to remember that Display scaling is applied to both layers and regardless of the type of tiles used and/or bitmap mode. That means we can't have one layer in 320x240 pixel mode overlayed by another layer in 640x480 pixels.
Layer
VERA supports two independent layers to be used. Layer 1 is the default text mode that is active at startup. Layer 0 is placed underneath it and it is disabled by default. They can each have different sizes, point to different tilesets, have different color depth and can be scrolled independently.
Map Base
Map Base is a pointer or address in VRAM where the layer data is stored therefore each layer has its own pointer to Map. Each layer’s configuration also tells VERA what kind of tile data to expect. Map Width and height is defined in multiples of tiles not pixels. For example the default screen is configured as 128 tiles wide and 64 tiles high and tiles are 8 x 8 pixels. If we do some math on these numbers we see that that would translate to 1024 x 512 pixels, which of course is larger than what can be displayed on the monitor (640x480).
It is also very important to remember that each tile takes two bytes. We will look at this in more detail later but if we simplify it contains both the index to tile and display attributes like color and horizontal and vertical flipping.
MapBase register is 8 bits and with 128 Megabytes of VRAM it is clear we can’t address any location but instead we can only set bits 9-16 and bits 0-8 are always 0.
The Map memory can therefore only start at increments of 512 bytes or $200 hex. So possible starts are at $00000, $00200, $00400, etc. Default text mode at startup is at $1B000.
Tile Base
Tile base is a pointer or address in VRAM where tiles are defined. How data is organized depends on the type of tiles we want to use. Tiles can be 8 or 16 pixels wide and 8 or 16 pixels high and any combination of them: 8x8, 8x16, 16x8 and 16x16. As mentioned before each layer can use different tiles that can also have different color depths 1, 2, 4 or 8 bits per pixel.
Of course both layers can use the same tiles by pointing to the same Tile base or share parts by having Tile Base pointers offset.
Similarly to Map Base also Tile Base can only address its location in steps. In this case we only have 6 bits available (bits 11-16) and can therefore only address VRAM in 2048 byte or $800 hex increments.
Possible starts are therefore: $00000, $00800, $01000, etc. Default font at startup starts at $1F000.
Tile Modes
We have following Tile modes at our disposal:
- 1 bit per pixel Tile Mode (16 colors)
- 1 bit per pixel Tile Mode (256 colors)
- 2 bits per pixel Tile Mode
- 4 bits per pixel Tile Mode
- 8 bits per pixel Tile Mode
Global Display settings
In order to be able to use both layers and change the resolution of the output let’s briefly check three of the registers that can be used to manipulate that:
Register | Address | Name | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|---|---|
9-0 | $9F29 | Video | Current Field | Sprite Enable | Layer 1 Enable | Layer 0 Enable | NTSC/RGB 240p |
Chroma Disable | Output Mode | |
10-0 | $9F2A | Horiz. Scale | Active Display Horizontal Scale | |||||||
11-0 | $9F2B | Vert. Scale | Active Display Vertical Scale |
We see several interesting things in these registers. They are also pretty self explanatory. For this tutorial the important are bits 4 and 5 in register 9 - Video ($9F29) register with which we turn on and of two available layers.
In Assembly we can use bitwise OR and AND to set or clear bits and therefore something like below can be used:
ora #%00010000 ; Set Bit 4 to Enable Layer 0
sta $9F29 ; Store new value to Video Register
Or
and #%11011111 ; Clear Bit 5 to Disable Layer 1
sta $9F29 ; Store new value to Video Register
And with registers 10 and 11 we set the Scale of the displayed image. Default values for both are 128 for 1x Scale or 640x480 resolution or 80x60 tiles (characters) if tiles are 8x8 pixels in size.
Layer Settings
As mentioned before we have two layers and everything we will say in this section applies equally to both. They both have 7 registers that are exactly the same. Just bear in mind that in order to see Layer 0, Layer 1 must have transparency which is always Color Index 0. Seven Layer registers are:
Register | Address | Name | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|---|---|
20 | $9F34 | Layer 1 - Config | Map Height | Map Width | T256C | Bitmap Mode | Color Depth | |||
21 | $9F35 | Layer 1 - Mapbase | Map Base Address Bits 9 - 16 | |||||||
22 | $9F36 | Layer 1 - Tilebase | Tile Base Address Bits 11 - 16 | Height | Width | |||||
23 | $9F37 | Layer 1 - H Scroll | Horizontal Scroll Bits 0 - 7 | |||||||
24 | $9F38 | Layer 1 H Scroll | - | Horizontal Scroll Bits 8 - 11 | ||||||
25 | $9F39 | Layer 1 - V Scroll | Vertical Scroll Bits 0 - 7 | |||||||
26 | $9F3A | Layer 1 - V Scroll | - | Vertical Scroll Bits 8 - 11 |
The name of Register 20 - Config already hints that it might be important. So let’s start with it. It defines core characteristics of the layer of which the size is determined using two bits for both height and width and is defined in tiles with following values:
01 – 64 tiles wide or high
10 – 128 tiles wide or high (default value)
11 - 256 tiles wide or high
So that means that each layer can have 16 possible dimensions: 32 x 32 tiles, 32 x 64 tiles, 32 x 128 tiles etc. all the way up to 256 x 256 tiles which is only a theoretical possibility because we simply don’t have enough VRAM available. If we add to this that each tile can be 8 or 16 pixels wide or high we have quite a lot of options.
To determine the Tile mode we combine values of three bits:
Bits 0 and 1 determine color depth:
00 - 1 bit per pixel01 - 2 bits per pixel
10 - 4 bits per pixel
Bit 3 is used to distinguish between two variations of 1 bit per pixel modes - 16 colors and 256 colors available per tile (or character). We will look at this in more detail later. Since we are working on tile modes we will assume that Bit 2 to turn on Bitmap mode will be 0.
All Tile modes at glance:
Since the layer size is defined in tiles it is also important to understand what is the size of tiles.
Tile sizes are defined in bits 0 and 1 of register 22.
Tile Height and Tile Width have following values:
1 – each tile is 16 pixels in size
This means that tiles can be 8x8, 8x16, 16x8 or 16x16 pixels.
Register 21 and 22 contain pointers to different parts of video memory where data is stored. Those are the pointers we described in the beginning. Let’s emphasize again that we can’t choose just any location in VRAM to start our map data or tile data but they have to align every 512 ($200) bytes for Map Base and every 2048 ($800) bytes for Tile base.
Map Memory Structure
We are almost ready to look at each Tile mode in more detail but let’s first check how the Map data is stored.
Map Base is pointing to the beginning of the Map Data. Map Data starts with the top left corner and each tile takes two bytes of memory. So first two bytes define top left tile on the screen, next two bytes define the one to the right of it and so on until we reach the width*2 and then the following two bytes define the first tile of the second row.
The content of the two bytes defining the appearance of each tile on the screen has two possible interpretations.
Tile memory Structure
Tile Base points to definition of tiles. We have four different depths to define each pixel in the tile which determines how many colors each Tile can have and as a consequence how much space they take in the memory.
The answer is already in the name of each Tile Mode:
In 1bpp each pixel only uses 1 bit, so one byte in Tile definition represents 8 pixels. 1 will be drawn in foreground color, 0 in background. If Tiles are 8x8 pixels first byte will represent top 8 pixels, next will represent second row and so on. In case of 16 pixel wide tiles the first byte represents the first eight pixels, the second one represents the rest of the pixels in the first row.
In 2bpp mode of course 2 bits are used so one byte only covers four pixels. And we need two bytes to represent 8 pixels. The rules for larger tiles are the same as for 1bpp.
4bpp and 8bpp follow the same logic with each more colorful variation allowing more flexibility but also much more demand for VRAM. Let’s compare space requirements:
Tile Mode | Tile Size | 1 Tile Memory | 256 Tiles Memory | 1024 Tiles Memory |
---|---|---|---|---|
1bpp Mode | 8x8 | 8 bytes | 2K | N/A |
16x16 | 32 bytes | 8K | N/A | |
2bpp Mode | 8x8 | 16 bytes | 4K | 16K |
16x16 | 64 bytes | 16K | 64K | |
4bpp Mode | 8x8 | 32 bytes | 8K | 32K |
16x16 | 128 bytes | 32K | 128K | |
8bpp Mode | 8x8 | 64 bytes | 16K | 64K |
16x16 | 256 bytes | 64K | 256K |
These are of course just for illustration and planning purposes. There is no requirement how many tiles we actually define and use but it gives us some idea which mode to use when we know how many different tiles our game might require. We also don't need to load all tiles in advance but can reload them based on the level of game for eaxmple.
1 bit per pixel Mode - 16 Colors
This is default Tile mode at startup of Commander X16. Character set is nothing more than 1bpp Tile set with each Tile being one character of PETSCII in 8x8 pixels.
Two bytes in Map data have following meaning:
Offset | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|
0 | Tile / Character Index | |||||||
1 | Background Color | Foreground Color |
We have 4 bits for foreground color and four bits for background color so we can address the first 16 colors from Palette for either.
1 bit per pixel Mode - 256 Colors
This is an alternative mode that can be used for text mode. It gives us access to all the colors in the palette with the limitation that only foreground can be chosen. Background is always transparent or black if Layer 0 is disabled.
Map Data contains following attributes:
Offset | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|
0 | Tile / Character Index | |||||||
1 | Foreground Color |
All 8 bits are available to choose any color from the Palette:
2 bit per pixel Mode - 4 Colors
Next three modes will be more appropriate for games and therefore their attributes are more targeted at typical tile sets. Especially 2 bit Tiles might be very popular in games where the space in VRAM might get tight as they still allow pretty colorful tiles but do not take too much space. Map Memory still uses two bytes per tile but content is quite a bit different:
Offset | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|
0 | Tile Index (bits 0-7) | |||||||
1 | Palette Offset | V-flip | H-flip | Tile Index (bits 8-9) |
We see that we have two more bits to address tiles so we can use up to 1024 different tiles and on top of that we can flip them horizontally and vertically. Color availability requires some planning and a custom palette would most likely be required in most games using this mode because most of the colors are out of range in this mode. Color is picked from Palette by combining 4 bits from the Map called Palette Offset to be used as high nibble and then two bits from the for the pixel value are added as bit 0 and 1. With 00 always transparent we only have three remaining colors available. Which those are let’s look at the illustration using default palette:
4 bit per pixel Mode - 16 Colors
This mode follows the same rules as 2 bpp one. The only difference of course is that now we have 4 bits per pixel available and can address the whole palette because 4 bits from Offset and four bits from Tile gives us a full 256 bytes range. Of course we can only show 15 plus a transparent per tile.
Data in Map is organized the same:
Offset | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|
0 | Tile Index (bits 0-7) | |||||||
1 | Palette Offset | V-flip | H-flip | Tile Index (bits 8-9) |
And color reach is as follows:
8 bit per pixel Mode - 256 Colors
The final and most colorful mode is also the most wasteful in terms of VRAM requirements and is probably not going to be used much in actual games but it is quite handy for demos.
Map Data is organized using same attributes as 2bpp Mode and 4 bpp Mode:
Offset | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|
0 | Tile Index (bits 0-7) | |||||||
1 | Palette Offset | V-flip | H-flip | Tile Index (bits 8-9) |
The colors are in principle very easy to determine with just few exceptions:
- Color 0 is again transparent, so binary value of %0000 0000 will simply show the color of the layer below (if there is one)
- Colors 1-15 will be affected by Palette Offset in Map data in the same way as in above example for 4bpp mode which makes sense because they only use 4 lower bits anyway and are simply complemented by top 4 bits from the offset.
- Colors 16-255 are not affected by the Palette Offset and are taken directly from the palette. Note that this is different from the behavior for the 8bpp sprites.
Example
We looked at all the modes and possibilities, now it finally time to write some code. The sample code contains 9 sections that can be seen as pretty much independent snippets that do only one thing at the time and should be pretty easy to understand. I don’t use any macros for clarity. The link to source code is at the end, let’s list the sections and what they do:
Section 1 - Customize standard character (tile)
We set up VERA registers to address the VRAM location to where the @ character is defined and we change it to smiley face.
Section 2 - Create a new custom tile
Very similar to the previous snippet with the difference that we can pick any VRAM address that is accessible with Tile Base pointer. We use 16x16 pixel and 256 color mode to define the brick background from the Crazy Boulder game.
Section 3 - Configure Layer 0
By default Layer 0 is disabled and all the pointers are set to 0. In this section we configure it so that Map Base is pointing to $04000 in VRAM and Tile Base to $12000 where the new custom tile is located.
Section 4 - Fill Layer 0 with Brick tiles
Now that we have set up Layer 0 and we have custom tile available we can fill the Map with “bricks”.
Section 5 - Turn the Layer 0 on
After Layer 0 is drawn, it takes a very simple step to turn it on by changing just one bit in one VERA register.
Section 6 - Change Layer 1 to 256 mode
To make it a bit more colorful let’s change the default text layer from 16 to 256 color mode. Agai, to achieve that we only need to change one bit in one VERA register.
Section 7 - Clear Layer 1
This operation can be easily done with one call to Kernal but here we do it by writing directly to VRAM which might be faster and provide some more flexibility. We are making sure that Layer 1 is transparent so we can see Layer 0 underneath.
Section 8 - Write to Layer 1
This section is a little bit longer because we have three loops to write some text and custom Smiley character drawn to Layer 1. Since we changed the Layer 1 to 256 color mode we are taking advantage of access to whole Palette for very colorful writing.
Section 9 - Change Display Scale
The last step is to reduce the display resolution from 640x480 to 320x240 by changing two VERA registers so the display is more easily readable.
The End result should look like this:
Link to source Code is here
The binary can be downloaded here
Just a quick nitpick for accuracy. In 4bpp and 8bpp modes, you cannot have 1024 tiles. The VERA does not have enough VRAM, as it only has 128K. To have 1024 8bpp tiles, the VERA would need 256K of VRAM just for the tiles, at which point you still wouldn't have any VRAM left for the tile map. Likewise 1024 4bpp tiles can technically fit in 128K, but would overwrite sprite attributes and palette data, and still not leave any space for an actual tile map. Realistically, the most tiles you can reasonably use in 8bpp is 256, and 512 for 4bpp. I know from experience :)
ReplyDelete