gbadev.org forum archive

This is a read-only mirror of the content originally found on forum.gbadev.org (now offline), salvaged from Wayback machine copies. A new forum can be found here.

Beginners > Help with tile mode and sample program

#31930 - ymalik - Thu Dec 16, 2004 1:33 am

Hello,
I have questions about the following sample program. The program is from Jonathan Harbour's book Programming the Nintendo Game Boy Advance. He does not explain some of the stuff in the program.
Code:

////////////////////////////////////////////////////////////
// Programming The Game Boy Advance
// Chapter 6: Tile-Based Video Modes
// TileMode0 Project
// main.c source code file
////////////////////////////////////////////////////////////

#define MULTIBOOT int __gba_multiboot;
MULTIBOOT

//include the sample tileset/map
#include "test.pal.c"
#include "test.raw.c"
#include "test.map.c"

//function prototype
void DMAFastCopy(void*, void*, unsigned int, unsigned int);

//defines needed by DMAFastCopy
#define REG_DMA3SAD *(volatile unsigned int*)0x40000D4
#define REG_DMA3DAD *(volatile unsigned int*)0x40000D8
#define REG_DMA3CNT *(volatile unsigned int*)0x40000DC
#define DMA_ENABLE 0x80000000
#define DMA_TIMING_IMMEDIATE 0x00000000
#define DMA_16 0x00000000
#define DMA_32 0x04000000
#define DMA_32NOW (DMA_ENABLE | DMA_TIMING_IMMEDIATE | DMA_32)
#define DMA_16NOW (DMA_ENABLE | DMA_TIMING_IMMEDIATE | DMA_16)

//scrolling registers for background 0
#define REG_BG0HOFS *(volatile unsigned short*)0x4000010
#define REG_BG0VOFS *(volatile unsigned short*)0x4000012

//background setup registers and data
#define REG_BG0CNT *(volatile unsigned short*)0x4000008
#define REG_BG1CNT *(volatile unsigned short*)0x400000A
#define REG_BG2CNT *(volatile unsigned short*)0x400000C
#define REG_BG3CNT *(volatile unsigned short*)0x400000E
#define BG_COLOR256 0x80
#define CHAR_SHIFT 2
#define SCREEN_SHIFT 8
#define WRAPAROUND 0x1

//background tile bitmap sizes
#define TEXTBG_SIZE_256x256 0x0
#define TEXTBG_SIZE_256x512 0x8000
#define TEXTBG_SIZE_512x256 0x4000
#define TEXTBG_SIZE_512x512 0xC000

//background memory offset macros
#define CharBaseBlock(n) (((n)*0x4000)+0x6000000)
#define ScreenBaseBlock(n) (((n)*0x800)+0x6000000)

//background mode identifiers
#define BG0_ENABLE 0x100
#define BG1_ENABLE 0x200
#define BG2_ENABLE 0x400
#define BG3_ENABLE 0x800

//video identifiers
#define REG_DISPCNT *(unsigned int*)0x4000000
#define BGPaletteMem ((unsigned short*)0x5000000)
#define SetMode(mode) REG_DISPCNT = (mode)

//vertical refresh register
#define REG_DISPSTAT *(volatile unsigned short*)0x4000004

//button identifiers
#define BUTTON_RIGHT 16
#define BUTTON_LEFT 32
#define BUTTON_UP 64
#define BUTTON_DOWN 128
#define BUTTONS (*(volatile unsigned int*)0x04000130)

//wait for vertical refresh
void WaitVBlank(void)
{
  while((REG_DISPSTAT & 1));
}

////////////////////////////////////////////////////////////
int main(void)
{
  int x = 0, y = 0;
  int n;

  //create a pointer to background 0 tilemap buffer
  unsigned short* bg0map =(unsigned short*)ScreenBaseBlock(31);
 
  //set up background 0
  REG_BG0CNT = BG_COLOR256 | TEXTBG_SIZE_256x256 | (31 << SCREEN_SHIFT) | WRAPAROUND;

  //set video mode 0 with background 0
  SetMode(0 | BG0_ENABLE);

  //copy the palette into the background palette memory
  DMAFastCopy((void*)test_Palette, (void*)BGPaletteMem, 256, DMA_16NOW);

  //copy the tile images into the tile memory
  DMAFastCopy((void*)test_Tiles, (void*)CharBaseBlock(0), 57984/4, DMA_32NOW);

  //copy the tile map into background 0
  DMAFastCopy((void*)test_Map, (void*)bg0map, 512, DMA_32NOW);

  //main game loop
  while(1)
  {
    //wait for vertical refresh
    WaitVBlank();

    //D-pad moves background
    if(!(BUTTONS & BUTTON_LEFT)) x--;

    if(!(BUTTONS & BUTTON_RIGHT)) x++;

    if(!(BUTTONS & BUTTON_UP)) y--;
 
    if(!(BUTTONS & BUTTON_DOWN)) y++;

    //use hardware background scrolling
    REG_BG0VOFS = y ;
    REG_BG0HOFS = x ;

    //wait for vertical refresh
    WaitVBlank();

    for(n = 0; n < 4000; n++);
  }

  return 0;
}

////////////////////////////////////////////////////////////
// Function: DMAFastCopy
// Fast memory copy function built into hardware
void DMAFastCopy(void* source, void* dest, unsigned int count, unsigned int mode)
{
  if (mode == DMA_16NOW || mode == DMA_32NOW)
  {
    REG_DMA3SAD = (unsigned int)source;
    REG_DMA3DAD = (unsigned int)dest;
    REG_DMA3CNT = count | mode;
  }
}


Now for my questions:
1. What do the count and mode paramaters do in DMAFastCopy()?
2. What are DMA_16 and DMA_32?
3. In tilemode, the 256x256 bitmap image was broken up into 8x8 tiles, like a jigsaw puzzle, right?
4. If the tile map (specified by test_Map) specifies where each tile (specified in test_Tiles) is stored in order to create the entire map, how come the program seems to scroll for "infinity?"

Thanks,
Yasir

#31932 - ScottLininger - Thu Dec 16, 2004 1:48 am

ymalik wrote:
1. What do the count and mode paramaters do in DMAFastCopy()?


I believe that count is how many "chunks" of data that DMA copies. Mode is how big the chunks are (32 bit or 16 bit).

Quote:
2. What are DMA_16 and DMA_32?


DMA_16 copies 16bit chunks are a time. DMA_32 copies 32. There are certain areas of memory that can only be copied with DMA_16 (such as VRAM, if I'm remembering right.) DMA_32 will obviously be faster in cases where you can use it.

Quote:
3. In tilemode, the 256x256 bitmap image was broken up into 8x8 tiles, like a jigsaw puzzle, right?


Yup.

Quote:
4. If the tile map (specified by test_Map) specifies where each tile (specified in test_Tiles) is stored in order to create the entire map, how come the program seems to scroll for "infinity?"


Because tile mode backgrounds "wrap around". So if you're at scroll position (256,256), you'll see the same exact screen as you do at (0,0).

What's more, the REG_BG0VOFS and REG_BG0HOFS registers, which contol scrolling, also "wrap around" as all unsigned integers do. So if you scroll to postion (64000++, 64000++) (or whatever the hell is the maximum value for that register), you're right back to (0,0).

Cheers,

Scott

#31933 - ymalik - Thu Dec 16, 2004 2:30 am

Thanks, Scott, for your fast response.
ScottLininger wrote:

I believe that count is how many "chunks" of data that DMA copies. Mode is how big the chunks are (32 bit or 16 bit).


mode is being passed using the define #define DMA_32NOW (DMA_ENABLE | DMA_TIMING_IMMEDIATE | DMA_32), so how can that be the size of the chunk?

#31966 - Lupin - Thu Dec 16, 2004 8:20 am

DMA_32 specifies the size of the chunk (32 bit)

DMA_ENABLE just enables your DMA transfer (obvious...)

DMA_TIMING_IMMEDIATE lets the GBA execute the DMA transfer immediately (also quite obvious)

There are also other constants that you can set for the mode parameter, allowing you to use DMA for nice effects or just for memory clearing
_________________
Team Pokeme
My blog and PM ASM tutorials

#32048 - ymalik - Fri Dec 17, 2004 3:24 pm

I have a question about following code which implements tiles and rotation
Code:

////////////////////////////////////////////////////////////
// Programming The Game Boy Advance
// Chapter 6: Tile-Based Video Modes
// RotateMode2 Project
// main.c source code file
////////////////////////////////////////////////////////////
#define MULTIBOOT int __gba_multiboot;
MULTIBOOT

#include "rotation.h"
#include "tiles.pal.c"
#include "tiles.raw.c"
#include "tilemap.h"

//prototypes
void DMAFastCopy(void*, void*, unsigned int, unsigned int);
void RotateBackground(int, int, int, int);

//defines needed by DMAFastCopy
#define REG_DMA3SAD *(volatile unsigned int*)0x40000D4
#define REG_DMA3DAD *(volatile unsigned int*)0x40000D8
#define REG_DMA3CNT *(volatile unsigned int*)0x40000DC
#define DMA_ENABLE 0x80000000
#define DMA_TIMING_IMMEDIATE 0x00000000
#define DMA_16 0x00000000
#define DMA_32 0x04000000
#define DMA_32NOW (DMA_ENABLE | DMA_TIMING_IMMEDIATE | DMA_32)
#define DMA_16NOW (DMA_ENABLE | DMA_TIMING_IMMEDIATE | DMA_16)

//background movement/rotation registers
#define REG_BG2X *(volatile unsigned int*)0x4000028
#define REG_BG2Y *(volatile unsigned int*)0x400002C
#define REG_BG2PA *(volatile unsigned short *)0x4000020
#define REG_BG2PB *(volatile unsigned short *)0x4000022
#define REG_BG2PC *(volatile unsigned short *)0x4000024
#define REG_BG2PD *(volatile unsigned short *)0x4000026

//background 2 stuff
#define REG_BG2CNT *(volatile unsigned short *)0x400000C
#define BG2_ENABLE 0x400
#define BG_COLOR256 0x80

//background constants
#define ROTBG_SIZE_128x128 0x0
#define ROTBG_SIZE_256x256 0x4000
#define ROTBG_SIZE_512x512 0x8000
#define ROTBG_SIZE_1024x1024 0xC000
#define CHAR_SHIFT 2
#define SCREEN_SHIFT 8
#define WRAPAROUND 0x1
#define BG_MOSAIC_ENABLE 0x40

//video-related memory
#define REG_DISPCNT *(volatile unsigned int*)0x4000000
#define BGPaletteMem ((unsigned short *)0x5000000)
#define REG_DISPSTAT *(volatile unsigned short *)0x4000004
#define BUTTON_A 1
#define BUTTON_B 2
#define BUTTON_RIGHT 16
#define BUTTON_LEFT 32
#define BUTTON_UP 64
#define BUTTON_DOWN 128
#define BUTTON_R 256
#define BUTTON_L 512
#define BUTTONS (*(volatile unsigned int*)0x04000130)
#define CharBaseBlock(n) (((n)*0x4000)+0x6000000)
#define ScreenBaseBlock(n) (((n)*0x800)+0x6000000)
#define SetMode(mode) REG_DISPCNT = (mode)

//some variables needed to rotate the background
int x_scroll=0,y_scroll=0;
int DX=0,DY=0;
int PA,PB,PC,PD;
int zoom = 2;
int angle = 0;
int center_y,center_x;

////////////////////////////////////////////////////////////
// Function: main()
// Entry point for the program
////////////////////////////////////////////////////////////
int main(void)
{
 int n;
 int charbase = 0;
 int screenbase = 31;
 unsigned short * bg2map = (unsigned short *)ScreenBaseBlock(screenbase);

 //set up background 0
 REG_BG2CNT = BG_COLOR256 | ROTBG_SIZE_128x128 |
              (charbase << CHAR_SHIFT) | (screenbase << SCREEN_SHIFT);

 //set video mode 0 with background 0
 SetMode(2 | BG2_ENABLE);

 //set the palette
 DMAFastCopy((void*)tiles_Palette, (void*)BGPaletteMem, 256, DMA_16NOW);

 //set the tile images
 DMAFastCopy((void*)tiles_Tiles, (void*)CharBaseBlock(0), 256/4, DMA_32NOW);

 //copy the tile map into background 0
 DMAFastCopy((void*)tiles_Map, (void*)bg2map, 256/4, DMA_32NOW);

 while(1)
 {
  while(!(REG_DISPSTAT & 1));

  //use the hardware to scroll around some
  if(!(BUTTONS & BUTTON_LEFT)) x_scroll--;
  if(!(BUTTONS & BUTTON_RIGHT)) x_scroll++;
  if(!(BUTTONS & BUTTON_UP)) y_scroll--;
  if(!(BUTTONS & BUTTON_DOWN)) y_scroll++;
  if(!(BUTTONS & BUTTON_A)) zoom--;
  if(!(BUTTONS & BUTTON_B)) zoom++;
  if(!(BUTTONS & BUTTON_L)) angle--;

  if(!(BUTTONS & BUTTON_R)) angle++;

  if(angle > 359)
   angle = 0;

  if(angle < 0)
   angle = 359;

  //rotate the background
  RotateBackground(angle,64,64,zoom);

  while((REG_DISPSTAT & 1));

  //update the background
  REG_BG2X = DX;
  REG_BG2Y = DY;
  REG_BG2PA = PA;
  REG_BG2PB = PB;
  REG_BG2PC = PC;
  REG_BG2PD = PD;

  while((REG_DISPSTAT & 1));
  for(n = 0; n < 100000; n++);
 }
}

////////////////////////////////////////////////////////////
// Function: RotateBackground
// Helper function to rotate a background
////////////////////////////////////////////////////////////
void RotateBackground(int ang, int cx, int cy, int zoom)
{
 center_y = (cy * zoom) >> 8;
 center_x = (cx * zoom) >> 8;
 DX = (x_scroll - center_y * SIN[ang] - center_x * COS[ang]);
 DY = (y_scroll - center_y * COS[ang] + center_x * SIN[ang]);
 PA = (COS[ang] * zoom) >> 8;
 PB = (SIN[ang] * zoom) >> 8;
 PC = (-SIN[ang] * zoom) >> 8;
 PD = (COS[ang] * zoom) >> 8;
}

////////////////////////////////////////////////////////////
// Function: DMAFastCopy
// Fast memory copy function built into hardware
////////////////////////////////////////////////////////////
void DMAFastCopy(void* source, void* dest, unsigned int count,
                unsigned int mode)
{
 if (mode == DMA_16NOW || mode == DMA_32NOW)
 {
  REG_DMA3SAD = (unsigned int)source;
  REG_DMA3DAD = (unsigned int)dest;
  REG_DMA3CNT = count | mode;
 }
}


What do the registers REG_BG2X, REG_BG2Y, REG_BG2PA, REG_BG2PB, REG_BG2PC, and REG_BG2PD do? I know that they control rotation, but what does each represent and do?

Thanks,
Yasir

#32056 - Cearn - Fri Dec 17, 2004 4:32 pm

This is what they do. And the specifics of affine backgrounds are explained a little further down

#32546 - ymalik - Thu Dec 23, 2004 8:08 pm

I have questions about main() and WaitVBlank():
1. What is the difference between vertical retrance and vertical blank?
2. Can anything be done during the vertical blank period? And why wait for the vertical blank?
3. Is there a reason to wait for the horizontal retrace?
4. Are REG_BG0V0FS and REG_BG0H0FS referenced from the center of the screen?

Thanks,
Yasir

#32557 - tepples - Thu Dec 23, 2004 9:29 pm

These signals come from the terminology of CRT and TV signals:
  • Vertical blanking ("vblank") means that time when no pixels are being drawn between one frame and the next.
  • Vertical retrace means that time when the electron beam is deflected toward the top of the display. This happens within vblank, and it takes time to complete because of how CRT electron guns work.
  • Vertical sync is the signal, embedded in the signal, that tells the CRT to start vertical retrace.
On LCDs, it doesn't take any time to start redrawing pixels at the top (thus no vertical retrace is needed), but display systems still insert a vertical blank so that the game can update the video memory while the video circuitry isn't using it. Waiting for vertical blank is useful because it gives you a consistent timer (guaranteed to occur every 280896 cycles, or about 59.7 times a second) on which to base animation, and it lets you know when you can update that animation without causing display artifacts (or on older 8-bit and 16-bit systems, without bus contention).

Waiting for horizontal blanking is useful when doing raster effects such as palette modification, image warping, XZ plane rotation and scaling, distance fog, and the like.

The background scroll registers, called REG_BG0VOFS and REG_BG0HOFS in many header files, are offsets from the upper-left corner of the screen. Increasing the values of these registers will make the pixels of the map move up and to the left respectively, just as scrollbars in Mac OS and Windows work.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#32564 - ymalik - Thu Dec 23, 2004 11:02 pm

Thanks, tepples, for your indepth answer.
How do you "update the video memory while the video circuitry isn't using it" during the vertical blank?

#32586 - ymalik - Fri Dec 24, 2004 4:21 am

Wait, would you use an interrupt? But I still don't see what you could have in the code for a vblank interrupt.

#32594 - tepples - Fri Dec 24, 2004 6:43 am

Most single-player games seem to use a game loop that boils down to this:
Code:
while(!done)
{
  wait_for_vblank();  /* synchronize to a time base */
  update_animation();  /* draw all characters in their new positions */
  check_controls();  /* translate keypresses into commands for the player character */
  run_behaviors();  /* move all characters according to game rules */
}

You need the vblank interrupt primarily because of a quirk in how the BIOS handles low-power mode.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#32642 - ymalik - Fri Dec 24, 2004 2:58 pm

Why does the first program that I posted have two calls to WaitVBlank(), one at the top of the loop and one at the bottom?

#32646 - identitycrisisuk - Fri Dec 24, 2004 3:48 pm

Oh yeah, I didn't notice that before. Looks like it's maybe trying to deliberately half the frame rate? That's the only reason I can think why it has the for loop doing nothing after it as well.
_________________
Code:
CanIKickIt(YES_YOU_CAN);

#32647 - Cearn - Fri Dec 24, 2004 3:53 pm

Like tepples said, "Waiting for vertical blank ...lets you know when you can update that animation without causing display artifacts", for small loops you really only need one, so the second is probably redundant. There are a number of little indiscrepancies in the book, you may have noticed he uses another WaitVblank in another chapter and ....

wait hang on ...

Quote:
Code:

void WaitVBlank(void)
{
    while((REG_DISPSTAT & 1));
}

Isn't REG_DISPSTAT&1 true when you're inside the VBlank period, meaning that this function actually waits till the VBlank is over, instead of starting?

Anyway, one of the problems with using REG_VCOUNT or REG_DISPSTAT for vsyncing is that you might still be inside the VBlank period when the next game-loop comes along. For example
Code:

while(1)
{
   while(!(REG_DISPSTAT&1));
   // REG_VCOUNT will be 160 here
   // do a LITTLE game stuff
   // REG_VCOUNT might be, say, 164 here
}

Now, at the next iteration, REG_DISPSTAT&1 will still be true, so you won't actually have waited for the next VSync at all and your timing will be all wrong. To remedy this, you could make sure you're in the next refresh already, like this:
Code:

while(1)
{
   while(!(REG_DISPSTAT&1));

   // REG_VCOUNT will be 160 here
   // do a LITTLE game stuff
   // REG_VCOUNT might be, say, 164 here

   while((REG_DISPSTAT&1));
   // REG_VCOUNT will be 0 (I think)
}

This is actually what the second demo uses. You could combine the two into one function like this
Code:

// wait for the next VBlank
void WaitVBlank(void)
{
   while((REG_DISPSTAT&1)); // wait till VDraw
   while((REG_DISPSTAT&1)); // wait till VBlank
}

In the end, though it's prefereable to use interrupts for this, specifically the BIOS VBlankIntrWait routine. The VBlank interrupt itself will only fire ONCE, so you don't have the still-in-VBlank problem. VBlankIntrWait has an additional benefit in that it saves battery power. Tonc has a demo on how to use it, but I don't know how to do it in HAM. I'd point you to other threads on the subject, but university is closing in 5 minutes, so I have to leave right about now (which reminds me: merry christmas and happy newyear, everyone)

#32673 - tepples - Fri Dec 24, 2004 10:33 pm

If you use VBlankIntrWait(), then you have to have at least a 2-file project: one for Thumb code (main()), and one for ARM code (the ISR). The inability to get interrupts going in a 1-file project is one of the reasons that a lot of homebrew demos wait for vblank with a while loop rather than with VBlankIntrWait().
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#32676 - ymalik - Fri Dec 24, 2004 11:18 pm

Why can't you have the ISR in the same file? Just point the memory address where the interrupts branch to to your ISR, right?
What are "display artifacts?"

#32688 - identitycrisisuk - Sat Dec 25, 2004 12:44 am

ymalik wrote:
Why can't you have the ISR in the same file? Just point the memory address where the interrupts branch to to your ISR, right?
What are "display artifacts?"


Yeah, I wouldn't mind a bit of clarification on this, the new VS6 thing that Abscissa posted about gives you the option to create a simple program that uses VBlankIntrWait in one file. The contents are as follows:
Code:
// main.cpp : Small sample project
//
// This program demonstrates:
//   - Setting up the screen mode
//   - Drawing a moving pixel
//   - Enabling VBlank interrupt
//   - Correct way to wait for VBlank, using
//     the BIOS's battery-saving VBlankIntrWait

#define REG_DISPCNT      *((unsigned short  *)0x4000000)
#define REG_DISPSTAT   *((unsigned short  *)0x4000004)
#define REG_IE         *((unsigned short  *)0x4000200)
#define REG_IF         *((unsigned short  *)0x4000202)
#define REG_IME         *((unsigned short  *)0x4000208)
#define REG_INTMAIN      *((unsigned short **)0x3007FFC)
#define REG_IFBIOS      *((unsigned short  *)0x3007FF8)

#define SCREEN          ((unsigned short  *)0x6000000)

 // Put InterruptHandler in speedy iwram
void InterruptHandler() __attribute__ ((section(".iwram")));

 // This must be Arm, not Thumb
void InterruptHandler()
{
   // Basically do nothing

   // Disable Interrupts
   REG_IME = 0x0000;

   // Reset REG_IF and REG_IFBIOS
   REG_IF |= 0x0001;
   REG_IFBIOS |= 0x0001;

   // Enable Interrupts
   REG_IME = 0x0001;
}

int main()
{
   int x=10;
   int y=20;

   // Setup screen mode: Mode 3, 1D Object Mode, BG2 and Objects on.
   REG_DISPCNT = 0x1443;
   
   // Turn on VBlank interrupt
   REG_DISPSTAT = 0x0008;
   REG_IE = 0x0001;

   // Enable Interrupts
   REG_INTMAIN = (unsigned short *)InterruptHandler;
   REG_IME = 0x0001;

   while(1)
   {
      // Wait for VBlank using BIOS funtion VBlankIntrWait
      // Remove "<<16" if using Thumb instructions
      asm volatile("swi 5<<16" ::: "r0", "r1", "r2", "r3");

      // Erase pixels
      SCREEN[(    y*240) + x  ] = 0x0000; // Black
      SCREEN[((y+1)*240) + x  ] = 0x0000;
      SCREEN[(    y*240) + x+1] = 0x0000;
      SCREEN[((y+1)*240) + x+1] = 0x0000;

      // Move pixels
      x += 2;
      if(x >= 230)
         x = 10;

      y += 1;
      if(y >= 150)
         y = 10;

      // Draw pixels
      SCREEN[(   y  * 240) + x  ] = 0x7FFF; // White
      SCREEN[((y+1) * 240) + x  ] = 0x7FFF;
      SCREEN[(   y  * 240) + x+1] = 0x7FFF;
      SCREEN[((y+1) * 240) + x+1] = 0x7FFF;
   }
}


I still need to read up a bit more on the differences between arm and thumb and when they can both be used, they're only now becoming slightly important to me.
_________________
Code:
CanIKickIt(YES_YOU_CAN);

#32689 - ymalik - Sat Dec 25, 2004 1:07 am

Chapter 9 of Programming the Nintendo Gameboy Advance has an example of having an ISR in the same file. It is just a simple C function. The interrupt is on hblank.

#32711 - tepples - Sat Dec 25, 2004 5:31 am

True, but now main() is still running as ARM code and eating battery power to access twice as large instructions from slow ROM, unless you put it in IWRAM too.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.