Sprites in Assembly I - Setup
One of fundamental tools in the game developer's toolbox are
hardware sprites and Commander X16 comes with a very strong and robust support.
The hardware takes a big chunk of workload and makes game programming
even more feasible. However it can be quite confusing to start with and
therefore in this short blog post I will explain how to configure sprites in Assembly.
We will take few basic steps and answer following questions:
- How to turn on the sprites
- Sizing and Colors
- Positioning
- Storing appearance
Important Addresses
Before we start coding let’s sketch video memory and registers that we will be using. As you probably know, VERA stands for Video Enhanced Retro Adapter and we can simply refer to it as video chip. We have to also understand that video memory is separate to main program memory and only way to access it is through a set of VERA registers.More details can be found here.
Important VERA memory areas again:
Address Range | Description | How will we use it |
---|---|---|
$000000 - $1FFFF | Video RAM | To store sprite graphics and settings |
$1FA00 - $1FBFF | Palette | We will use default palette in our sprite |
$1FC00 - $1FFFF | Sprite Attributes | To define appearance of our sprites and to point to sprites definition and position on screen |
Video RAM
Video RAM is 128K of memory that can be used for display and
for storing other video related data that needs to be accessed by VERA. We will
use it to store sprite graphics. Remember that Sprites are independent from
other displayed data so it is very common to display Sprites over or under text, which is sometimes referred to as tile
modes or also over or under graphics modes. Let’s remind us how much memory standard text modes use. At startup the Layer 0 is disabled and Layer 1 is used as text mode in
80 x 60 Character mode: It uses 256 bytes per
line so total of 7,650 bytes or $2C00 in hexadecimal. It starts at VRAM address $1B000 and therefore ends at $1EBFF
So for our purpose if we want to use one of these two text
modes we can safely store sprite data anywhere in VRAM bank 0 ($00000 - $0FFFF) after address $3C00.
Palette
This area of memory is used to define the available colors
for our sprites. We will use default 256 colors available to us and might look
into tricks that can be done with it in some future post.
Sprite Attributes
This is the most important part of the video memory related
to sprites. Commander X16 supports up to 128 hardware sprites. They all get
exactly 8 bytes to get their attributes defined and therefore occupy 1024 bytes
in memory area $1FC00 - $1FFFF:
Sprite 0: $1FC00
- $1FC07
Sprite 1: $1FC08
- $1FC0F
Sprite 2: $1FC10
- $1FC17
…
Sprite 127: $1FFF8
- $1FFFF
So what are the attributes?
Offset / Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
0 | Pointer to sprite graphics (bits 5-12) | |||||||
1 | Mode | Pointer to sprite graphics (bits 13-16) | ||||||
2 | X Position | |||||||
3 | X Position | |||||||
4 | Y Position | |||||||
5 | Y Position | |||||||
6 | Collision Mask | Z Depth | V-Flip | H-Flip | ||||
7 | Height | Width | Palette Offset |
We will use and therefore check in more detail the ones color coded in the picture above. Green ones are very straight forward, blue a bit more complicated and the red one is the trickiest and we will therefore spend most time on it.
Before we start…
One more thing before we start coding. In BASIC and C we have VPOKE and VPEEK commands/functions to write to and read from VRAM. We don't have that in Assembly. Instead we have to use VERA registers directly in order to be able to write to VRAM.
This must seem like disadvantage but in reality VPOKE are very inefficient and the more data we need to transfer to VRAM the more inefficient they become.
Before every byte written to VRAM we have to tell VERA to which address we want to write it. Since VERA has 128K of memory we need 17 bits to address it meaning we have to write 3 bytes to VERA registers before we can even transfer one byte of data. In addition to that we have to tell VERA which Data register will we use.
Let's for example write Assembly equivalent to VPOKE 0,$4000,255 (BASIC) or vpoke(255,0x4000); (C):
stz $9F25 ; VERA_CTRL set to use DATA0 register
stz $9F22 ; VERA_HIGH bit 16 is set to 0
lda #$40
sta $9F21 ; VERA_MID set to $40
stz $9F20 ; VERA_LOW set to $00
lda #255
sta $9F23 ; Write 255 to VRAM address $04000
As we see there is significant overhead in setting up code for writing to VERA and we were even able to optimize by using stz instead of lda and sta instructions. It is important to remember that every time we call VPOKE from BASIC or vpoke from C those settings have to be updated.
In Assembly, on the other hand, we often need to write more than one byte to VRAM, in fact very often we write hundreds or thousands of bytes in sequence. Here one of VERA's most useful features comes to play. We can configure it in such a way to autoincrement the address at every write or read.
Let's imagine a scenario where we have to write 255 ten times to addresses from $04000 - $04009:
stz $9F25 ; VERA_CTRL set to use DATA0 register
lda #%0001000 ; VERA_HIGH Increment is set to 1
sta $9F22 ; and address bit 16 is set to 0
lda #$40
sta $9F21 ; VERA_MID set to $40
stz $9F20 ; VERA_LOW set to $00
lda #255
sta $9F23 ; Write 255 to VRAM address $04000
sta $9F23 ; Write 255 to VRAM address $04001
sta $9F23 ; Write 255 to VRAM address $04002
sta $9F23 ; Write 255 to VRAM address $04003
sta $9F23 ; Write 255 to VRAM address $04004
sta $9F23 ; Write 255 to VRAM address $04005
sta $9F23 ; Write 255 to VRAM address $04006
sta $9F23 ; Write 255 to VRAM address $04007
sta $9F23 ; Write 255 to VRAM address $04008
sta $9F23 ; Write 255 to VRAM address $04009
I believe this example clearly show why addressing VERA directly might feel a bit more cumbersome in the beginning but the more data we transfer between VRAM and CPU RAM the more efficient it becomes vs BASIC. It has to be said that in C we can implement both mechanisms.
Turning Sprites on
Sprites can be turned on in two steps. First we set the
master enable flag on by setting bit 6 in the VERA Register DC_VIDEO ($9F29) to 1.
lda $9F29
ora #%01000000
sta $9F29
Next we can turn on individual sprites by setting Z-Depth
values for each one we want to display. If we check above Z-Depth is stored in
bits 2-3 at offset 6, so for Sprite 0 that would be $1FC06, Sprite 1 would be
$1FC0E and so on.
We need two bits because (as the name suggest) we control
not only on-off but also the depth in relation to screen layers. It has the
following possible values:
Value | Description |
---|---|
00 | Disabled |
01 | Between Background and Layer 0 |
10 | Between Layer 0 and Layer 1 |
11 | In front of Layer 1 |
Since the default text screen is rendered on Layer 1 we have to use the last value to display sprite on top like this:
stz $9F25 ; VERA_CTRL set to use DATA0 register
lda #%0000001 ; VERA_HIGH Increment is set to 0
sta $9F22 ; and address bit 16 is set to 1
lda #$FC
sta $9F21 ; VERA_MID set to $FC
lda #$06
stz $9F20 ; VERA_LOW set to $06
lda #%00001100
sta $9F23 ; Write $0C to VRAM address $1FC06
The problem is that we still don’t see anything because we
haven’t defined any other attributes, position on screen and of course the
graphics of the sprite.
Sizing and Colors
Sprites can have 4 different sizes horizontally and
vertically and any combination of those. Just like for the depth we have two
bits to determine the size:
Value | Description |
---|---|
00 | 8 pixels |
01 | 16 pixels |
10 | 32 pixels |
11 | 64 pixels |
We set the dimensions in the upper four bits of the eighth byte of the Sprite attributes. Bits 6-7 for height and bits 4-5 for width. So for example if we want 8x8 sprite we set all four bits to 00. If we want sprite that is 32 pixels wide and 16 pixels high we set it to %0110.
Lower four bits are used for palette shifting by 16
positions every time one is added, which can be quite useful but we will not
explore it now, but feel free to experiment with it.
So, what are the colors that are at our disposal? By default
in first 16 position we have standard Commodore 64 colors, next 16 are
grayscale from black to white and the rest are different scales.
Only exception is color 0, which is always transparent and
it is the only color that is transparent.
More about colors and palettes here.
The last thing about colors is to decide how many do we want to have in our sprite. We have two possibilities 16 or 256 and therefore we use either 4 bits per pixel or full byte per pixel. We set this in Mode setting – Bit 7 of Offset byte 1 with following values:
More about colors and palettes here.
The last thing about colors is to decide how many do we want to have in our sprite. We have two possibilities 16 or 256 and therefore we use either 4 bits per pixel or full byte per pixel. We set this in Mode setting – Bit 7 of Offset byte 1 with following values:
0 – 4 bits per pixel
1 – 8 bits per pixel
Clearly we have to find the balance between how many colors
do we want to display per single sprite and how much space sprite occupies in
memory. At first glance it seems that 4 bits and only 16 colors are very
limited, however also in that case we have all 255 colors at our disposal and
using palette shifting each sprite can have access to different part of palette
if needed.
Positioning
We can position sprites anywhere on the screen. Since the
maximum screen resolution of Commander X16 is 640x480 we need at least 10 bits
to be able to store all values and that is exactly what we have.
In Offset byte 2 we store bits 0-7 of X (horizontal) coordinate
with remaining 2 high bits in Offset byte 3. Similarly we store Y (vertical)
coordinate in Offset byte 4 (bits 0-7) and Offset byte 5 (high bits).
If we have horizontal position in variable X, we could use
following commands to store position to correct registers for Sprite 0.
stz $9F25 ; VERA_CTRL set to use DATA0 register
lda #%0010001 ; VERA_HIGH Increment is set to 1
sta $9F22 ; and address bit 16 is set to 1
lda #$FC
sta $9F21 ; VERA_MID set to $FC
lda #$02
stz $9F20 ; VERA_LOW set to $02
lda #XposLO
sta $9F23 ; Write Low byte to VRAM address $1FC02
lda #XposHI
sta $9F23 ; Write High byte to VRAM address $1FC03
We have to treat both positions as signed binary values. That means that we can set them as negative numbers in order to make sprites "disappear" to the left or top of the screen. We are of course still working 10 bit values so the following table with few examples should make it more clear:
Decimal Binary Hexadecimal
0 00 00000000 $00 $00
1 00 00000001 $00 $01
100 00 01100100 $00 $64
-1 11 11111111 $03 $FF
-10 11 11110110 $03 $F6
So if we want the sprite to be offset 10 pixels oof the left edge of the screen we have to write value $03 to high byte of X position (offset 5) and $F6 to lower byte of X position (offset 4).
We could consider Z-Plane as part of positioning in Z
coordinate too. We talked about different options in the section above.
Storing Appearance
Sprites are essentially small rectangular graphic areas and
we are already armed with all the information we need to understand the possibilities:
- Horizontal x vertical dimensions can be 8, 16, 32 or 64 pixels
- Each pixel can take 1 byte or half byte (4 bits)
- We understand the available colors in palette (and know where to change them if needed)
The only tricky part is to store the pointer to sprite
graphics into sprite registers because it is slightly unconventional. We are
used to round all addresses to full bytes or at least nibbles (4 bits or half
bytes) because that makes it convenient for displaying in hexadecimal notation.
However pointer to sprite graphics is 17 bits but we can only store 12 so lower
5 bits are always 0. That means we can point to memory locations in increments
of 32 bytes.
Video RAM is located from $00000 - $1FFFF – 16 KB, so
obviously with 17 bits long address we can reach all of it. We also already
determined that screen starts at $00000 and in default text mode (SCREEN 2) we
are safe anywhere after $03C00.
So how do we calculate the pointer?
OK, let’s assume we have our graphics data located from
$04000. In binary that is:
%0 0100 0000 0000 0000
If we cut away lower 5 bits we get:
%0 0100 0000 000
or properly grouped into nibbles and bytes:
10 0000 0000
Or in Hex:
$200
So all that remains is to write $00 to Offset 0 and $02 to Offset 1 (plus the Mode flag in the bit 7). I personally would prefer to have address shifted by 4 bits so it would be more clearly readable in Hex notation but I guess after a while one can get used to it.
Putting it all together
I think it is time to put all the knowledge together and use
it in practical example. Current version of the emulator has an interesting
feature built in, namely Sprite 0 is used to display mouse pointer. When mouse pointer is over the emulator
window it shows Sprite 0 at its location. By default the Sprites are off and
there is no graphics associated with it so nothing is shown but let’s define it
and turn it on so we see its position. By default it comes with plain black pointer but we can change that. First turn it on like this:
MOUSE 1
MOUSE 1
I think it is appropriate to use Amiga style pointer:
- We will therefore use 16 x 16 pixel sprite.
- We will use 256 color mode
- For Black we will use color index 16, for dark red index 50 (color $a22) and for pale red/tan index 38 (color $fbb).
- Since we did hard calculations above already, we will use $04000 to store the data. Here is the complete code:
To review..
In lines 10-14 we have necessary directives to tell assembler how to build the binary. It will generate a BASIC line to call the machine code.
Lines 17-21 are some constants we use to make the code more readable.
In section from 29 -34 we prepare VERA registers for storing sprite data in VRAM starting at $04000 and then in lines 36-40 we transfer 256 bytes of data from data section at the end of the program into VRAM.
In lines 43-45 we set the Bit 6 in register VIDEO to turn on the sprites. This is actually redundant if we turned on the mouse pointer in BASIC but it is good practice nevertheless.
Then in lines 47-51 we configure VERA registers to point to the beginning of Sprite 0 registers which is $1FC00.
And finally in lines 53-63 we set all eight registers for Sprite 0.
To compile this program we have to save it as Sprite1.asm and using cc65 compiler and command line:
cl65 -t cx16 Sprite1.asm -o SPRITE1.PRG
This generates the SPRITE1.PRG binary that can then be simply loaded to CommanderX16 using
LOAD"SPRITE1.PRG"
and run simply by
RUN
or for better visibility reduce the screen resolution by typing
SCREEN 3
RUN
Comments
Post a Comment