Sunday, December 14, 2014

Adventures in 32X Homebrew - Part 3: 2D Graphics

Previous: Part 2: 32X Hello World

Drawing Stuff

Now that I had the ability to compile a program, run it and get it to display something, the next step to making my masterpiece game would be to get the thing to display some graphics.

Here is one of the first "working" attempts I made at creating a 2D sprite and animating a few frames of action with 32X.

While this sort of worked, there were problems with my implementation that were visibly apparent. As there is no standard library for graphics when working with 32X you pretty much need to implement your own.

Principles

To keep in mind the objective, in order to draw something to the screen on any system you will need to the following tasks.
  1. Convert source images to a format and size that can be processed by tools for 32X, including the right resolution and color depth that our hardware supports.
  2. Provide a mechanism to represent the image data in our ROM which we can reference from our code.
  3. In the game code be able to load the graphic image data to RAM.
  4. At the appropriate times in the game logic draw the image from memory to specified coordinates in the frame buffer.
  5. When finished rendering everything in the frame, flip the frame buffers.
Recall the 32X Video specs:
Video Memory: 256Kb (two 128 Kb framebuffers)
Screen Resolution: 320x224 or 320x240 (PAL)
Color depth: Up to 15 bits, 32768 unique colors, two-bytes per pixel, can use paletted color (Packed Pixel, 8BPP) mode, using up to 256 on-screen colors with one byte per pixel to save memory.

If you use the Packed Pixel mode, you will save on memory (RAM) to store your image files or your the read only memory in your ROM. I started with this approach thinking I might run out of memory on the 32X with all the 2d sprite graphics I wanted to display for my game. In my case I wanted to try to make a "16-bit plus" kind of game, so a ton of colors wasn't really necessary.

Most of this information I have taken from the excellent reference in Devster's Guide to the 32X.

Drawing Blocks

I thought the best way to start giving examples how to draw things on the screen for 32X was to not involve the whole image formatting/importing processing that would need to be done to load an image into your ROM during build time, and to reference the image data, load into memory and draw it out in your game code. Instead will begin with drawing colored blocks of pixels to the screen procedurally, instead of going loading an image. In this example we'll look at setting an RGB color in the color palette memory and drawing rectangles to the 32X framebuffer to create the images on the display.
Starting with the hello world tutorial code from the previous post, I am going to invoke some new methods that we didn't use before.

Global Variables and Constants

Just to note for further sections, the following global variables were added in the main.c

#define BLOCK_COLOR_1 32
#define BLOCK_COLOR_2 34


int r1 = 6;
int g1 = 0;
int b1 = 0;


These were used to set the color for the blocks when they are drawn.

Setting up the Color Palette

In this demo, like HelloWorld, we continue to use the 256 color, packed pixel mode, as a reminder this mode is set in the main.c before the main game loop using function Hw32xInit(MARS_VDP_MODE_256, 0);. The palette is a memory buffer called the CRAM, it holds up to 256 colors, each color being a 16 bit word of memory that can be any of the supported 32768 colors. The color in memory is stored in RGB format with the following bits:
15             0
PBBBBBGGGGGRRRRR

Bits 0-4: Red Value
Bits 5-9: Green Value
Bits 10-14: Blue Value
Bit 15: Represents a flag for "priority" of whether to draw 32X in front of Genesis VDP or behind.

For each of the Red, Green, Blue values you have 5 bits, which allows you to specify a numeric value for each color from 0-31, where 0 is basically black and 31 would be the "full" color (this is how you get 32768 colors, 32 red x 32 green x 32 blue = 32768 total possible color combinations).

To set a value in the CRAM, you need to write this data to that memory location, offset by the index of the color, you then refer to that index when drawing pixels of that color to the main framebuffer used to display. The CRAM can be referenced directly using MARS_CRAM define from 32x.h. It is the memory location: 0x20004200. However, there are utility functions which you may use in hw_32x.h,
extern void Hw32xSetFGColor(int s, int r, int g, int b);
extern void Hw32xSetBGColor(int s, int r, int g, int b);


To set a color "white" in the CRAM, at index 1 for example you may do the following:
//set block color in CRAM
Hw32xSetFGColor(1,31,31,31);

To set additional colors at other indices:
Hw32xSetFGColor(BLOCK_COLOR_1,r1,g1,b1);
Hw32xSetFGColor(BLOCK_COLOR_2,31-r1,31-g1,31-b1);

In this case, I use the global int variables I defined above, and the defines as constants to keep track of which index is being used for which color in the CRAM. In my demo I will vary the values of these ints from 0-31 to get various different combinations of colors which I will use when drawing the blocks.

While you can certainly set up all the values you want to use in your color palette up front, before your main game loop, you can also change them dynamically on the fly, for a "palette" swap effect without having to have a different image in memory.

Setting up Line Table in 32X Framebuffer

Now that we have the colors we want to draw in the CRAM. We need to write to the framebuffer what colors we want to draw at what pixels. There are two DRAM framebuffers in the 32x hardware, and so the general flow of your program is to write to one of the framebuffers, set a flag to "flip" the framebuffers, this causes the 32X hardware to write one of the framebuffers to the display while allowing you to know write to the other framebuffer again, and so on.

The drawing works by writing to memory locations in the framebuffer, in 256 color mode, for each pixel you have to write one byte for the index of the color in the CRAM. For the Direct Color mode (15 BPP) you need to write two bytes to draw a pixel. The location where that pixel is drawn depends on what location in the framebuffer memory location you write to. Determining which memory address is what on-screen pixel is sometimes complicated. First thing to understand is something called the Line Table which informs the 32X VDP where in the framebuffer each line of pixels should start, and for each line the VDP will display the next 320 pixels starting from the line address. The Line table itself is just the first 256 words of the framebuffer, so the first line of the display is generally after that 256th line table address. Here is a diagram of the relationship between line table and framebuffer lines, if you think about X, Y coordinates each line refers to an X on the screen, while the 320 bytes after the line address are the Y coordinates for pixels.
The line table doesn't say what addresses you need to point to, so the above example uses no offset, each line is right after the previous one in contiguous memory. In my own development, I came across a number of issues where objects being written to either the start or end of the line might go past line ending and then wrap around the the start of the next line. In some cases this made it difficult to fix without a lot of expensive checking. What I ended up doing was putting an offset of 4 words before and after every line, and updating the Line table accordingly.

This padding enabled my drawing code to be more efficient and allow some pixels to be written into the padding without corrupting the next line. All of my drawing functions in my sample currently depend on this four word offset. To set up the Line Table, in my main.c at the end of every loop I call
        //draw the 32X framebuffer line table with offset of 4
        drawLineTable(4);


This function is defined in 32x_images.c.

Drawing Rectangle to the Framebuffer

With all that work so far, now comes the "easy" part, drawing the darn thing. Objective here is to write out pixel data to the line address in memory, and to place the pixels at the write point in the line so it shows up in the correct place on the screen. In my code, I do all the heavy lifting for this inside the 32x_images.c's
void drawFillRect(const int16 x, const int16 y, const int xWidth,  const int yWidth, vu8* color)
void drawRect(const int16 x, const int16 y, const int xWidth,  const int yWidth, vu8* color)

These functions will draw a rectangle to the x, y pixel coordinates in the display, with a certain height and width in pixels, using a pointer to a color array. Wait, why is the color parameter a pointer to an array? Well this is basically a performance optimization that was suggested by the SpritesMind forum folks. Basically the fastest possible memory operation are when you write 8 bytes in one shot, so when filling or drawing a rectangle we are going to copy 8 bytes of a single color value from the palette to the framebuffer. To set up the color array I did this before the game loop.
vu8 blockColor[8] = {BLOCK_COLOR_1,BLOCK_COLOR_1,BLOCK_COLOR_1,BLOCK_COLOR_1, BLOCK_COLOR_1,BLOCK_COLOR_1,BLOCK_COLOR_1,BLOCK_COLOR_1};

This array references just the index number for the color in the CRAM we want to use. I then use this in some calls to draw the colored rectangles inside of the game loop.
drawRect(10,70,296,144,(vu8*)&whiteColor);
drawFillRect(20,80,40,40,(vu8*)&blockColor);
drawFillRect(60,80,40,40,(vu8*)&blockColor2);


Flipping the Framebuffers

To get the rectangles to display you need to flip the framebuffer after everything has been drawn to it. To trigger the flip you need to set a special register at the end of the main game loop.

//flip the FB, without waiting on flip
currentFB ^= 1;
MARS_VDP_FBCTL = currentFB;


This can actually take some cycles, so you want to ensure that your code doesn't start trying to drawn anything to the framebuffer again until the flip has completed. Use the following check to wait for the flip to complete near the start of the main game loop, before you draw anything.
   
// wait on flip to finish
while ((MARS_VDP_FBCTL & MARS_VDP_FS) != currentFB) {}


Final Game

Putting it all together, and adding some dynamic color changes by updating the palette with new RGB values each game loop iteration, I created a simple flashing effect. Take a look at it on youtube.


Source code for this project is on GitHub 32XHombrew : drawingblocks

Feel free to send any feedback on this or other tutorials. There is so much information it is hard to put it all down on paper in a cohesive way. Hopefully these are useful to the next person who wants to try out 32X programming.

1 comment:

  1. Sharkoon RGB Flow Gaming Case in UAE, ATX Gaming Case in UAE, Gaming Case in UAE
    https://gccgamers.com/sharkoon-rgb-flow.html
    Sharkoon Flow Case in UAE, Safe Shopping Multiple Payment Options Express Delivery GCC Gamers Moneyback Guarantee.
    1632374598912-8

    ReplyDelete