Post

Alocação de Back Buffer para Software Renderer

Passos que precisamos para alocar um BackBuffer.

  1. Garantir o game loop (usando variáveis como running)
  2. Criar a janela: obter o tamanho da janela e re/criar o BackBuffer sempre que houver um resize da janela
  3. Renderizar: Vamos atualizar (copy) os dados do back buffer na área da janela quando ocorrer o WM_PAINT (depois do WM_SIZE)

Como sair de um main-loop

Ao tentar fechar uma janela, ela ainda continuará em execução.

Para finalizar o main-loop podemos enviar uma mensagem de PostQuitMessage no evento de close da janela. Isso fará o GetMessage receber a mensagem de WM_QUIT, saindo do looping.

Ou, uma outra forma (que vamos usar) seria definir uma variável global running.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case WM_CLOSE: {
    // PostQuitMessage(0); 
    // indica ao sistema que a thread requer 
    // o evento de (quit)
    // com uma variavel 'running' podemos controlar 
    // o exit como mostrar um pop-up ao sair do jogo (salvar)
    running = false;
} break;

case WM_DESTROY: {
    running = false;
} break;


while(running)
{
   // ...
}

Usando a lib Gdi do Windows para renderizar um Bitmap

Gdi é a biblioteca nativa do Windows que nos permite alocar um buffer para Bitmap.

Vamos criar um BackBuffer sempre que houver um resize (ou criação se for a primeira vez) da janela.

Obtendo o tamanho da janela.

Dentro do WM_SIZE podemos buscar o tamanho da janela com o GetClientRect e passar os valores de largura e altura para a função que criará o backbuffer.

Para quem já usou GLSurfaceView do Android irá reconhecer esse padrão de criar o buffer somente após ter o tamanho da janela a partir do resize.

1
2
3
4
5
6
7
8
9
10
case WM_PAINT: {
    RECT client_rect;
    GetClientRect(hwnd, &client_rect);
    
    int width  = client_rect.right  - client_rect.left;
    int height = client_rect.bottom - client_rect.top;
    
    // vamos recriar o bitmap quando a 'window' mudar de tamanho
    resize_dib_section(width, height); 
} break;

Criando e Recriando o BackBuffer

Vamos criar um bitmap com algumas configurações e disponibilizá-la junto a um * void que será nossa memória de bitmap. O Windows irá renderizar usando a sua API gdi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
internal void 
resize_dib_section(int width, int height) 
{
  if (bitmap_handle) {
    // Substituiremos para o VirtualFree
    DeleteObject(bitmap_handle);
  }

  if (!context) {
    context = CreateCompatibleDC(0);
  }

  bitmap_info.bmiHeader.biSize   = sizeof(bitmap_info.bmiHeader); // size of bitmap header
  bitmap_info.bmiHeader.biWidth  = width;
  bitmap_info.bmiHeader.biHeight = height;

  // padrao 1 porque agora nao precisa mais fazer a separacao das cores
  // em 'planos' como red-plane, blue-plane, etc.
  // resolvido com 'biBitCount - bits-per-pixel' 
  bitmap_info.bmiHeader.biPlanes        = 1; 
  bitmap_info.bmiHeader.biBitCount      = 32; // bit-per-pixel (rgba bits 8-8-8-8)
  bitmap_info.bmiHeader.biCompression   = BI_RGB;
  bitmap_info.bmiHeader.biSizeImage     = 0; // zero if BI_RGB
  bitmap_info.bmiHeader.biXPelsPerMeter = 0; 
  bitmap_info.bmiHeader.biYPelsPerMeter = 0; 
  bitmap_info.bmiHeader.biClrUsed       = 0; // qtd de index de cores (usadas na tabela de cores)
  bitmap_info.bmiHeader.biClrImportant  = 0; // usado na tabela de cores
  // bitmap_info.bmiColors[1] - usado para a paleta DIB_PAL_COLORS (indexes colors)

  // Substituiremos para o VirtualAlloc
  bitmap_handle = CreateDIBSection(context,
                                   &bitmap_info,
                                   DIB_RGB_COLORS, // or DIB_PAL_COLORS 16-bit
                                   &bitmap_memory, // **void
                                   0,  // for file-mapping 'backup'
                                   0); // for file-mapping 'backup'
}

Note que essa função é chamada sempre que houver um resize da tela. Então, precisamos livrar a memória anterior antes de criar uma nova.

Para isso usamos o DeleteObject quando houver um identificador (handle) do HBITMAP disponível.

The DeleteObject function deletes a logical pen, brush, font, bitmap, region, or palette, freeing all system resources associated with the object. After the object is deleted, the specified handle is no longer valid.

Essa função deve ter um autocast pois ela aceita vários tipos incluindo o HBITMAP retornado quando se criar um DIBSection.

Globals

Toda variável fora do escopo é visível durante a execução do programa. Logo, ela é Global. Agora uma variável global é aberta onde outros arquivos (translation units) podem acessá-las se houver a declaração de extern em um dos arquivos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// main.c
bool running;

void change_running();

int main() {
  change_running();
}
  
// global.c
extern bool running;

void change_running()
{
  running = !running;
}

g++ main.c global.c

Isso indica que, para mantermos mais restrito ao arquivo, ou seja, private file, devemos anotar a variável global com static. No nosso caso indicando internal para ela.

1
2
3
4
5
6
7
#define internal static

internal bool running; // usado no main-loop
internal BITMAPINFO bitmap_info; // usado para criar e atualizar o backbuffer
internal void *bitmap_memory; // usado para criar e atualizar o backbuffer
internal HBITMAP bitmap_handle; // usado para garantir um novo backbuffer
internal HDC context; // usado para criar o backbuffer

Atualizando a Janela com o BackBuffer

Uma vez criado/recriado o buffer a partir do tamanho da janela, devemos forçar a atualização desse buffer (swap) para realmente conseguir visualizar os pixels na tela. Ou seja, a função a seguir irá copiar os dados de memória do buffer para a área da janela no evento de WM_PAINT.

Esse evento só ocorre depois de um resize.

Dentro do evento WM_PAINT vamos chamar a função update_window().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case WM_PAINT: 
{
	// busca as dimensoes da janela a partir do identificador 'hwnd'
	PAINTSTRUCT paint;
	HDC deviceContext = BeginPaint(hwnd, &paint);

	int x = paint.rcPaint.left;
	int y = paint.rcPaint.top;
	int width  = paint.rcPaint.right  - paint.rcPaint.left;
	int height = paint.rcPaint.bottom - paint.rcPaint.top;

	update_window(deviceContext, x, y, width, height);

	EndPaint(hwnd, &paint);

} break;

A função update_window irá pedir para o ‘Windows’ copiar nosso buffer para a área da janela durante o looping (drawing) e escalar se necessário.

1
2
3
4
5
6
7
8
9
10
11
internal void 
update_window(HDC context, int x, int y, int width, int height) 
{
    StretchDIBits(context,
                  x, y, width, height, // dest
                  x, y, width, height, // src
                  bitmap_memory, // const VOID *lpBits
                  &bitmap_info, // const BITMAPINFO *lpBitsInfo
                  DIB_RGB_COLORS, // or DIB_PAL_COLORS 16-bit
                  SRCCOPY); // raster operation
}

Importante

Podemos atribuir um “alias” para as keywords em C como: #define global static

Static também inicializará os valores automaticamente com zero / false.

Significados de static

  1. Quando definido fora do escopo será uma variável global: global
  2. Quando definido dentro de um escopo (função) será parecido com global. Ou seja, irá persistir o dado contudo somente visível aquele escopo: local persist
  3. Quando definido em uma função, tornará esta função visível somente ao arquivo atual. E quando dizemos arquivo, podemos considerar o Translation Unit: internal

Release de dados

Não precisamos usar recursos como construtor/destrutor para livrar dados ao finalizar um programa. Esse processo já será feito pelo sistema operacional.

Muitos programadores, principalmente aqueles da programação orientada a objetos querem “livrar” espaços ao finalizar um main-loop, mas não há necessidade de gastar o tempo do usuário neste momento.

Em outras palavras, o usuário precisa fechar o jogo imediatamente!

Referências

Esta postagem está licenciada sob CC BY 4.0 pelo autor.