PS2 Linux Programming
2D Texture Mapping
Introduction
This tutorial will provide
guidance on the use of 2D texture mapping. A 24-bit bitmap files of size
256x256 pixels will be load into the main memory of the PS2 then transferred to
GS memory. The use of transparency and alpha blending will also be
investigated.
The PS2 comes with 4MB
(4096K) of video memory, often called VRAM, GSMem, or VMEM. This is a
relatively small amount of memory and using it requires careful thought and
design. Within the architecture of the PS2, however, it is possible to transfer
textures from main memory to VRAM at high speed (maximum rate of 2.4Gb/sec) and
this transfer can be maintained successfully within the game loop. As will
become evident, the architectural structure of the PS2 has implications on how
graphics/games application are designed - considerable differences exit for
example, between applications written for the PS2 and applications written for
a PC.
Under SPS2 in VESA
graphics mode, the front and back frame buffers are 640*480*4 = 1200K making a
total of 2400K of video memory being used for the frame buffers. The Z Buffer
is also 640*480*4 = 1200K, so out of the 4096K of video memory 3600K is already
used up, leaving just 496K for textures. The scenario is even worse if the PAL
video mode is used. PAL buffers are 640 *512*4 = 1280K. This means that 3840K
of video memory is used for the frame and z buffers leaving only 256K for
textures. 256k just so happens to be the size of a single 256 by 256, 32-bit
colour image. As mentioned above, the big advantage of the PS2 is its memory
bandwidth, so constantly moving textures from RAM into VRAM is not too much of
a problem.
In the sample code
provided with this tutorial, routines are provided for loading 256*256 24-bit
windows .bmp files. Normally, it is not necessary to use such a high-resolution
texture (a 24 bit colour depth is wasted on a TV) but since a monitor is
possibly being used, and since rendering is in 2D, the method is acceptable.
Usually several palletised 8-bit or even 4-bit images can be loaded into VRAM
at one time, but the use of a palette is beyond the scope of this tutorial and
will be discussed in a later tutorial. For now, the use of 24-bit images is a
simple way of getting textures loaded and on to the screen.
The texture loading code
is contained in the files texture.cpp and texture.h. The loading code is in two
parts: the first function LoadBitmap() loads a bitmap file from disk into SPS2
allocated memory; the second function UpLoad() uploads or transfers the image
data from main memory into graphics memory so that it can be used by the
graphics synthesiser.
Looking at main.cpp, it
can be seen that at the start of this file some #defines are used to help partition
the SPS2 memory that will be allocated. PRIMITIVEMEM_4K points to the start of
a 4k block that will be used to store the graphics primitive packets;
TEXTUREMEM_256K point to the start of a 256k area that will be used to store
the texture image; TEXTUREDMAMEM_4K points to the start of a 4k block that will
be used to store the DMA tags necessary to control the transfer of the image
into VRAM. TOTALMEMNEEDED is the total memory that needs to be allocated, which
in this case is 4+256+4 = 264K. The memory layout is illustrated in Table 1.
4k Primative memory |
256k Texture memory |
4k Texture DMAC tag
memory |
Still within main.cpp, the
function sps2Allocate() is used to allocate 264k of SPS2 memory. The
LoadBitmap("test.bmp", &pcMemory[TEXTUREMEM_256K], false)
function takes three parameters; the first is the bitmap file to be loaded, the
second is a pointer to the area of SPS2 memory that the file is to be loaded
into and the final parameter indicates whether or not RGB of (r=0, g=0, b=0)
should be made transparent or not. Note that it is possible to make colours
other than black act as transparent if required.
Look at texture.h it can
be seen that the structure BitmapFile contains two headers that are laid out in
exactly the same way as in an actual bitmap file, so it is possible to read
them directly from the disk file into this structure. The compiler directive
__attribute__((packed)) tells G++ not to insert any padding into the structure;
otherwise the struct would not be laid out in exactly the same format as in the
file. The reason G++ inserts padding by default is because aligned data is
faster on the PS2, but since loading an image will only be done at load time
performance is not too important here.
LoadBitmap() opens the
bitmap file and reads in the bitmap file header and the bitmap info header.
Details on the meaning of the parameters in the file and info headers are
contained in texture.h. Checks are made to verify that the file being read is a
bitmap file and that the bitmap is the correct dimensions and colour depth. The
pixel data from the file is then read in from the file and stored in SPS2
memory. Bitmaps are stored upside-down and in BGR format. This means that the R
and B components must be swapped to get the correct RGB format. Also a bitmap
file stores the image upside down, so the image data is read in and written
into SPS2 memory in reverse order or the correct way up. Note also that an
8-bit alpha channel is added to each pixel and if transparent black is to be
used, the alpha channel for pure black pixels is set to zero – the implications
of this will be discussed later in the tutorial.
Once this function has
completed, an image of dimensions 256x256 pixels of 24 bit colour depth with an
8 bit alpha channel (32 bits per pixel in total) is situated in SPS2 memory.
Note that this function
should only be called once at the start of the program. The image file should
generally remain in main memory for the duration of the program and can be
uploaded into graphics memory when required for rendering.
The function prototype for
Upload() is as follows:
void Upload(uint32
uGSPage, char * pcTexture, void * pDMAMem)
This function uploads a
texture from SPS2 memory into GS memory. The GS Page address (uGSPage) tells
the function where to put the texture in GSmem. GS memory is split up into a
number of pages, each page being 8192 bytes (8k) long. This means that in total
there are 512 pages in GS memory. The function sps2UScreenGetFirstFreeGSPage()
is used to get the page that comes directly after the z-buffer.
The pcTexture parameter
should point to the start of the texture data in SPS2 memory that was loaded
with LoadBitmap(). Finally, the pDMAMem pointer should point to 4k of SPS2
memory that the function can use to build its DMAC chain. The DMAC chain is
used to control the data transfer and 4k is sufficient memory for the size of
image to be transferred.
In principle, uploading
the texture is simply a case of setting 4 transmission registers on the GS,
then putting the image data into a GS Packet and firing it off. A full
description of this process is described in Chapter 4 of the GS Users manual
but the main points will be investigated here.
The first thing is to set
the 4 transmission registers. This is done using A+D mode in a GS packet. A+D
(address plus data) mode tells the GS that the data following the GIFtag
contains both the address of the register to be loaded and the data to be
loaded into the register. In packed mode, the bottom 64 bits contains the data
and the register address is contained within the top 64 bits.
The first register to be
set is BITBLTBUF. A so-called host to local transfer (i.e. EE to GS memory) is
being done so only the second half of this register, which deals with the
destination of the transfer, needs to be set. The first parameter DBP, is the
address of where to put the texture in GSmem. This address was passed into the
Upload() function as a GSPage address. However, DBP must be given in units of
“word address / 64”. This unit is rather confusing and it is easier to leave
“word addresses” out and just say (address / 256). Therefore each unit of DBP
is equal to 256 bytes. To put it another way,
1 Page address =
8192 bytes
1 (word-address / 64) = 256 bytes
From this it can be seen
that 32 (word-address / 64)’s make up 1 page address. So to convert our page
address to the format required by DBP we multiply it by 32 – this is done at
the start of the Upload() function.
The next field of
BITBLTBUF to be set is DBW (destination buffer width). This is given in units
of pixels / 64. Since a 256 pixel wide texture is to be loaded a texture buffer
of at least 256 pixel wide is needed - so the DBW parameter is set to (256 /
64) = 4. Finally the DPSM parameter, which is the destination pixel storage
format, must be set. A 32 bits per pixel texture is being used, so DPSM is set
to PSMCT32, which is defined as 0.
The next register to be
set is TRXPOS. This register specifies the offset coordinates at the upper left
point of the transmission area in the transmission buffer and the pixel
transmission direction. When transmitting from host to local, only the settings
for the destination buffer are of concern along with the DIR parameter, which
specifies the order that the pixels are transmitted. The first two fields (SSAX
and SSAY) are the X and Y coordinates of the top left source rectangle – these
are not needed in this case and can both be set to default values of 0. The
next two parameters are the X and Y coordinates of the top left destination
rectangle (DSAX and DSAY), which are both set to 0. The final parameter (DIR)
is set to 0, which specifies that the transmission order be from upper left to
lower right.
The next register is TRXREG.
This register is set with the width and height of the texture in pixels.
Finally there is TRXDIR.
As was pointed out above, a host to local transfer is being done, so the XDIR
parameter of this register is set to 0.
Now that the texture
transfer registers have been configured it is possible to send a Packet
containing the texture to the GS. This is done by configuring a GIFTag in image
transfer mode (GS_FLG_IMAGE). In image mode, all of the parameters of the
GIFtag have no meaning apart from NLOOP and QWC. The qword count (QWC) is set
to the size of the texture in quad words and the FLG parameter is set to
GS_FLG_IMAGE; all of the other GIFtag parameters are set to zero. The texture
data then ends the packet.
Unfortunately, due to the
SPS2 4k memory boundary things are not quite as easy as this in practice. REF
tags are used to point the DMAC to each individual 4K (256 qword) chunk of the
texture. Remember, the DMAC reads the REF tag then goes off and transfers the
specified amount of data from memory, then comes back to its original position
and reads the next tag.
The final part of this
function sets 4 GS registers using A+D mode. The first register to be set is
TEXFLUSH. It doesn’t matter what is written to this register since when the GS
detects that data is written to this register it flushes the texture cache.
This is similar to flushing the SPS2 cache, except here the texture data is to
be written through to GS memory instead of a DMA chain to SPS2 memory.
Next is the ALPHA_1
register. The alpha value of a pixel determines how transparent a pixel is. An
alpha value of 0x80 is fully opaque, and an alpha of 0 is fully transparent. A
number of different blending equations can be used by setting the different
fields of ALPHA_1. Creating custom blending equations is out of the scope of
this tutorial, but details can be found on page 100 of the GS Users manual. The
equation used in this tutorial is as follows:
Output colour = (Cs – Cd)
* As >> 7 + Cd
Where:
Cs = Source colour or
texture colour,
Cd = Destination colour or
colour presently in the frame buffer,
As = Source alpha or
texture alpha value.
As can be seen, the
multiplication is only shifted right 7 bits, which is why 0x80 is fully
blended. This means that alpha values of 0xFF will double the source colour.
Overloading both the alpha value and the vertex colours will allow the
brightness of a texture to be increased by up to 4 times. Note that to enable
alpha blending on a primitive, the ABE field (parameter 5) of the PRIM register
must be set to 1 (see later).
Next is TEX1. This
register sets the texture filtering mode. Mipmapping is not being used so the
first parameter (LCM) is set to 1 (fixed mipmap value). Parameters 3 and 4 are
the texture magnification and minification filters respectively. Both are set
to 1 to indicate linear sampling. This means that the texture unit will sample
the four pixels surrounding a position and average them together. This should
produce nicer looking images than simple point sampling would do.
Now for TEX0. The first
two parameters should be familiar, as they are the same fields that were set in
BITBLTBUF. They should have the same values as those set in BITBLTBUF so that
the texture unit uses the texture that was just uploaded. The next field PSM
should be set to 0 (PSMCT32) as 32 bit textures are being used. TW and TH are
the texture width and height given as 2 to the power of X. Since we are using
256 by 256 textures we want to set these both to 8 (2^8 = 256). The implication
of this is that the PS2 only supports texture sizes that are powers of two. To
load a texture that is an uneven power it is necessary to fill the rest of the
texture with black and load it as a power of two. The TCC field is set to one,
which means that the GS will use the alpha component from the texture. The last
parameter of interest is the TFX parameter. This is set this to MODULATE, which
means that the texture colour is multiplied by the vertex colour to obtain the
final colour. The PS2 does colour multiplication in a slightly different way to
normal:
Ordinary colour
multiplication:
Final colour = (col1 * col2) >> 8;
0xFF = (0xFF * 0xFF) >> 8;
PS2 colour multiplication:
Final colour = (col1 * col2) >> 7;
0xFF = (0xFF * 0x80) >> 7
The reason the PS2 does
this is so that it is easy to “double” the brightness of a texture to simulate
a bright light shining on it by specifying vertex colours larger than 0x80. If
a final colour is worked out to be larger than 0xFF it is clamped to 0xFF. What
this means is that if the texture is to be display at normal brightness on a
sprite, the vertex colour should be set to RGB (0x80, 0x80, 0x80).
As mentioned previously,
LoadTexture() should only be called once at the start of the program (or at
load time if you wish to use a loading screen). Upload() should be called each
time a different texture is to be loaded into GSmem, this being because there
is only room in GSmem to hold one texture. If more than one texture could be
put in GS memory at a time, it would only be necessary to set TEX0 to point to
the other texture before drawing. TEX0 could be set using A+D mode as done in
the upload function. Alternatively it is possible to put TEX0 into a GS batch.
Now that all of the
registers have been configures and the DMAC chain is complete, the DMAC chain
is flushed to SPS2 memory then the packet is fired off to the GS. Once the
packet has been transferred to GSmem the function returns and is complete.
Returning now to the main
program code (main.cpp) it can be seen that after the texture is uploaded in to
GSmem, a GS packet is built to display a textured sprite on screen. The
additional flags in the PRIM field of the GIFtag that must be set are the TME
(parameter 3: texture map enable), which is set to ON, or 1. Since 2D drawing
is being done U and V coordinates are used to specify the texture coordinates.
The default setting is STQ, so to change this the FST field (parameter 7) of
the PRIM register is set to 1 for UV. The GS batch is configured to contain the
vertex colour first (GIF_REG_RGBAQ) then GIF_REG_UV before GIF_REG_XYZ2. The
appropriate information is put into the primitive data, which comes after the
GIFtag.
UV coordinates are very
simple. UV (0, 0) corresponds to the top left of the texture. Each UV
coordinate corresponds to a pixel right and down respectively. So with the 256
* 256 pixel textures the UV coordinate (255, 255) would be the pixel in the
bottom right. UV coordinates should be given to the GS in 12:4 fixed point
number format, so the numbers must be shifted left by 4 (this is done in the
function in AddRegisters.h).
The final part of the code
is the render loop which has been seen before. In essence, the primitive packet
is fired off to the GS every frame using the DMAC in normal mode.
Texture mapping has been
illustrated using only 24 bit bitmap files with dimensions of 256 by 256 pixel.
This may not be the most efficient method (for use in commercial games) but for
the purposes of this tutorial it helps explain many of the required techniques
and concepts. It also allows for the creation of PS2 Linux based games using
texturing and other techniques such as texture animation, which will be
described in later tutorials.
Postscript
The
last parameter passed with the LoadBitmap() function determines whether true
black (rgb = 000) is transparent or not. In the code supplied the parameter is
set to “false” and the text, which is written in true black, appears black.
Change the final parameter in LoadBitmap() to “true” and the text will be
transparent – it will be possible to “see” the background colour through the
text. The use of transparency is fundamental to the creation of many of the
effects found in 2D and 3D games.
Dr Henry S Fortuna
University of Abertay
Dundee