티스토리 뷰

공부 이야기

[NDS] 프레임 버퍼 컨트롤

판다(panda) 2009. 1. 20. 00:02
개발 튜토리얼이 있는 사이트 입니다.
http://www.double.co.nz/nintendo_ds/index.html

해보시다가 안되는 점은 GBA Station 이라는 네이버 까페에 가보시면

도움되는 글이 많이 있을겁니다.

DS상의 스크린은 다양한 모드를 설정할 수 있습니다.

프레임 버퍼 모드는 직접 그릴 수 있는 가장 간단한 방법입니다.

프레임 버퍼는 스크린과 맵핑된 메모리 영역입니다. 

그 메모리 영역에 데이터를 쓰면 스크린상에 결과가 나타납니다.

이 모드를 사용할때 각각의 픽셀은 2바이트로 표현됩니다. 

C언어에서 16bit unsigned integer 와 상응하는 그 데이터는 555 포멧으로 표현됩니다.

이 55 포멧으로 직접 색을 변환할 필요는 없고 RGB 15라는 바로 쓸 수 있는 메크로 함수가 있습니다.

각각의 픽셀은 Red, Green, Blue 로 표현되는데 0에서 31까지의 범위를 갖습니다.

0은 색이 없는 것이고 31은 최대 컬러 값입니다.

RGB15 Color
RGB15(31,0,0) Red
RGB15(0,31,0) Green
RGB15(0,0,31) Blue
RGB15(0,0,0) Black
RGB15(31,31,31) White

아래 코드는 프레임버터의 시작 포인터에 blue 색을 지정하는 예제입니다.

uint16* framebuffer = ...;
     for(int i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; ++i)
         *framebuffer++ = RGB15(0,0,31);
 
닌텐도DS의 2D 하드웨서 가속기능을 바로 사용할수 있습니다.

하지만 프레임버퍼의 단점이라면..

sprites, tiled maps, scrolling, etc 등을 모두 직접 코딩해야 한다는 것입니다.

스크린 기능

DS는 아래 스크린은 터치기능이 있는 스크린입니다.

메인 스크린과 서브 스크린이라고 부르는 각각의 스크린을 프로그래밍을 해줘야 합니다.

이 예제는 하드웨어의 위에 화면인 메인 스크린을 사용합니다.

videoSetMode 라는 함수를 이용해아 모드를 설정할 것인데..

이것은 double buffering, page flipping 같은 것을  할 수 있습니다.

MODE_FB0 로 프레임버퍼를 사용할 것입니다.

videoSetMode(MODE_FB0);

프레임버퍼의 메모리영역은 여러개의 VRAM 이라고 불리는 영역으로 설정되어 있습니다.

첫 VRAM 영역은 VRAM_A 로 불립니다.

vramSetBankA(VRAM_A_LCD);

shape 그리기

단색으로 간단한 사각형을 그려보겠습니다.

void draw_shape(int x, int y, uint16* buffer, uint16 color)
{
   buffer += y * SCREEN_WIDTH + x;
   for(int i = 0; i < shape_height; ++i)
   {
       uint16* line = buffer + (SCREEN_WIDTH * i);
       for(int j = 0; j < shape_width; ++j)
       {
           *line++ = color;
        }
    }
}

프레임 버퍼는 메모리상에 여러 행으로 배치되는데

그래서 200픽셀 이상된다면 프레임 버퍼상의

첫 200개의 unit16 은 첫 스크린행이 되고 두번째 200개의 unit16 이 두번째 행이 됩니다.

참고로 SCREEN_WIDTH 와 SCREEN_HEIGHT 는 ndslib에서 매크로로 제공하여

스크린 넓이와 높이를 반환합니다.

shape_height 와 shape_width 는 테스트 목적으로 알맞은 정적 변수로 변경 가능합니다.

static int shape_width = 10;
static int shape_height = 10;
 
shape 움직이기

스크린을 가로지르는 움직임을 표현하기 위해서는 현재 위치의 shape 를 지우고

새로운 위치에서 다시 그릴 필요가 있습니다.

static int old_x = 0;
static int old_y = 0;
static int shape_x = 0;
static int shape_y = 0;

지우려는 배경색을 가진(이 경우에는 검정색) draw_shape 함수에게는 간단한 문제입니다.

새로운 위치와 shape 색을(이 경우에는 빨강색) 지정하고 다시 이 함수를 호출하면 됩니다.

draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));

여기서 주의할 점은 프레임 버퍼를 초기에 설정했던 VRAM_A 값을 넘겨줘야 한다는 것입니다.

간단한 main 함수로 만들어서 실행해보면

비스듬한 모양이 화면을 빠르게 가로지르는 것을 볼수 있을 것입니다.

int main(void)
{
   powerON(POWER_ALL);
   videoSetMode(MODE_FB0);
   vramSetBankA(VRAM_A_LCD); 

   while(1)
   {
       old_x = shape_x;
       old_y = shape_y;
       shape_x++;
       if(shape_x + shape_width >= SCREEN_WIDTH) 
       {
           shape_x = 0;
           shape_y += shape_height;
           if(shape_y + shape_height >= SCREEN_HEIGHT) 
           {
               shape_y = 0;
           }
       }      
       draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
       draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));
   }
}

좋지 않은 결과



DeSmuME 로는 보이지 않아서 iDeaS 에뮬을 사용했습니다.

Vertical Blank Interrupt

비스듬한 모양으로 보이는 이유는 스크린 표시방식 때문입니다.

하드웨어 장치는 매 1/60초 마다 다시 그립니다.

각각의 픽셀과 행과 행을 찾아 다니며 프레임 버퍼의 내용을 하드웨어 스크린 픽셀에 복사합니다.

이런 작업이 일어나는 동안, main 내부에서 스크린에 써진 프레임버퍼 내용을 변경합니다.

그래서 하드웨어가 지우기전에 막 다시 그린다면, 즉시 지워지지 않을 것입니다.

하드웨어가 그리기 바로전에 새로운 shape를 그린다면..
 
전에 shape의 부분과 새로운 shape의 부분이 남아 있을 것입니다.

고맙게도 하드웨어는 스크린에 그리는 것을 끝마쳤을때 우리에게 알려주는 방법을 가지고 있습니다.

이것을 Vertical Blank Interrrupt 라고 부릅니다.

우리는 이것이 발생할때 불려지는 함수를 기록할수 있습니다.

이 인터럽트는 또다른 뭔가를 하도록 빠르게 함수를 호출하기 위해

현재(예제 에서 main 함수에서 루프안에서 실행되는 동안)

행해지는 것을 막을수 있는 하드웨어 매커니즘입니다.

인터럽트 함수가 반환 되었을때 전에 활동이 결코 막지 않는다면 계속 진행될 것입니다.

전에 본것 같은 그리는 문제를 막기 위해서는,

하드웨어가 스크린상에 프레임버퍼의 내용을 넣지 않을때 한번에 프레임버퍼에 그리기를 원합니다.

최고의 타이밍은 vertical blank interrupt 하는 동안입니다.

인터럽트 셋업

인터럽트 작동 방법에 관한 자세한 내용은 나중에 다루겠지만,

인터럽트 코드에서 발생하는 것을 간단하게 개요를 말하겠습니다.

먼저 인터럽트가 발생했을때 우리가 호출하길 원하는 함수를 닌텐도 DS에 알려줄 필요가 있습니다.

void InitInterruptHandler()
{
   REG_IME = 0;
   IRQ_HANDLER = on_irq;
   REG_IE = IRQ_VBLANK;
   REG_IF = ~0;
   //DISP_SR = DISP_VBLANK_IRQ;
   REG_DISPSTAT = DISP_VBLANK_IRQ;
   REG_IME = 1;
}

이 간단한 코드에서 우린 단지 VBlank 인터럽트 발생을 원합니다.

on_irq 함수는 그것이 발생했을때 알려 줄것입니다.

on_irq 함수는 main 함수에서 앞에서 했던 프레임버퍼에 그리는 것을 할 것입니다.

void on_irq()

   if(REG_IF & IRQ_VBLANK)
   {
      draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
      draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));
      
      // Tell the DS we handled the VBLANK interrupt
      VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
      REG_IF |= IRQ_VBLANK;
      }
   else
   {
      // Ignore all other interrupts
      REG_IF = REG_IF;
    }
}

이 함수의 주요 부분은 main 함수의 while 루프에서 전에 했던 그리기입니다.

이 함수의 나머지는 인터럽트를 다루는 것입니다.

우린 또한 VBLANK 인터럽트를 다룰 DS 하드웨어에게 알려줄 필요합니다.

나중에 이유를 설명하겠지만 이것은 나중에 사용하여 호출할 swiWaitForVBlank를 요구합니다.
 
그 코드는 다음과 같습니다.

// Tell the DS we handled the VBLANK interrupt
    VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
    REG_IF |= IRQ_VBLANK;

여전히 좋지 않은 결과

우리는 main 함수에서 그리는 코드를 제거 할수 있습니다.

int main(void)
{
   powerON(POWER_ALL);
   videoSetMode(MODE_FB0);
   vramSetBankA(VRAM_A_LCD);
   InitInterruptHandler();
   while(1)
   {
       old_x = shape_x;
       old_y = shape_y;
       shape_x++;
       if(shape_x + shape_width >= SCREEN_WIDTH)
       {
          shape_x = 0;
          shape_y += shape_height;
          if(shape_y + shape_height >= SCREEN_HEIGHT)
          {
              shape_y = 0;
           }
        }     
     }
}

불행히도 실행에는 문제가 있습니다. 체커보드 모양이 스크린상에 반복에서 나타납니다.

다행이도 이유는 명확합니다.
 
하드웨어는 vertical blank 인터럽트가 발생할때 매 1/60초마다 on_irq 함수를 호출합니다.
 
shape 가 지워지고 다시 그려질때 입니다.

shape가 그려질 좌표가 증가할때 가능한 빠르게 실행됩니다.

on_irq 루틴이 호출되기전 50회가 될것입니다.

그 결과로 그리는 루틴은 마지막으로 shape가 그려진후

50번 갱신된 old_x, old_y 에서의 shape를 지웁니다.

그래서 잘못된 영역을 지웁니다.

올바른 결과

우리는 인터럽트가 발생할때 까지 sleep 하도록 while 루프에 요청하는 것으로 고칠수 있습니다.
 
이것은 while 루프가 덜 바쁘게 만드는 부가적인 효과를 얻을수 있습니다.

ARM8 프로세서는 인터럽트가 발생할때 까지 효과적으로 속도를 줄일수 있습니다.

이 함수가 swiWaitForVBlank 입니다.

VBLANK 인터럽트를 처리하여 register를 세팅하는 on_irq 내부를 앞서 기억할 것이다.

만일 register를 세팅하지 않았다면 swiWaitForBlank는 결과 발생하지 않을 인터럽트를

핸들링하기 위한 통지를 기다리며 하드웨어는 hang 걸릴 것입니다.

예제 프로그램에 이 한 라인을 추가해야 합니다.

int main(void)
{
   powerON(POWER_ALL);
   videoSetMode(MODE_FB0);
   vramSetBankA(VRAM_A_LCD);
   InitInterruptHandler();
   while(1)
   {
       old_x = shape_x;
       old_y = shape_y;
       shape_x++;
       if(shape_x + shape_width >= SCREEN_WIDTH)
       {
           shape_x = 0;
           shape_y += shape_height;
           if(shape_y + shape_height >= SCREEN_HEIGHT)
          {
              shape_y = 0;
           }
       }     
       swiWaitForVBlank();
   }
   return 0;
}

인터럽트 부분을 추가한 소스는 컴파일시 에러가 났습니다.

Makefile이 최신버젼의 devkitpro와 맞지 않는 것 같습니다.

컴파일 되어 있는 nds 파일을 dualis 에서 실행해 봤습니다.