3.1 - Alocando Buffer de Desenho com DIBSection no Windows
Table of Contents
Neste artigo vou alocar um back buffer no Windows usando CreateDIBSection.
A ideia é simples: o jogo desenha primeiro em um bloco de memória fora da tela e, depois, copia esse bloco para a janela. Essa cópia costuma ser chamada de blit.
Então temos duas etapas principais:
- Descobrir o tamanho da área de desenho da janela.
- Criar uma memória de pixels com esse tamanho e copiá-la para a tela quando necessário.
Essa é a continuação natural depois de criar a janela e pintar a área cliente com PatBlt. Agora, em vez de pedir para o GDI pintar um retângulo preto direto na janela, vamos controlar cada pixel manualmente.
Área cliente da janela
A janela do Windows possui bordas, barra de título e outros elementos que não fazem parte da área real de desenho.
Para um jogo, normalmente queremos o tamanho interno da janela. Ou seja, a área onde nossos pixels realmente serão exibidos.
Para isso usamos GetClientRect.
RECT client_rect; GetClientRect(hwnd, &client_rect); int width = client_rect.right - client_rect.left; int height = client_rect.bottom - client_rect.top;
O RECT possui quatro campos:
- left: posição inicial no eixo X;
- top: posição inicial no eixo Y;
- right: posição final no eixo X;
- bottom: posição final no eixo Y.
Como estamos usando GetClientRect, normalmente left e top serão zero. Mesmo assim, calcular right - left e bottom - top deixa a intenção explícita.
Faremos essa consulta quando a janela for criada e também quando ela for redimensionada. Se o usuário muda o tamanho da janela, o nosso buffer antigo não representa mais o tamanho correto da tela. Logo, precisamos destruir o bitmap antigo e criar outro.
Criando o back buffer
O bloco principal deste artigo é a função abaixo.
internal HBITMAP bitmap_handle;
internal void *bitmap_memory;
internal BITMAPINFO bmi;
internal void
create_back_buffer(int width, int height)
{
if (bitmap_handle) {
DeleteObject(bitmap_handle);
bitmap_handle = NULL;
bitmap_memory = NULL;
printf("Delete memory!\n");
}
assert(width > 0);
assert(height > 0);
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
bitmap_handle = CreateDIBSection(NULL,
&bmi,
DIB_RGB_COLORS,
&bitmap_memory,
NULL,
0);
printf("Create memory!\n");
if (!bitmap_handle) {
// TODO(tiago): handle error with GetLastError.
printf("Failed to Create Bitmap Handle\n");
return;
}
unsigned int *memory = (unsigned int *)bitmap_memory;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
*memory = (unsigned int)(0 << 16 | (unsigned char)y << 8 | (unsigned char)x);
memory++;
}
}
}
Aqui estamos criando um bitmap de 32 bits por pixel. Cada pixel ocupa 4 bytes na memória.
O valor escrito no loop final gera um gradiente simples. Ele não é o desenho definitivo do jogo. É apenas uma maneira rápida de confirmar se a memória foi criada corretamente e se conseguimos enxergar alguma coisa na tela.
O que é uma DIBSection
DIB significa Device Independent Bitmap.
Ou seja, é um bitmap independente de dispositivo. Ele não pertence a uma janela específica, nem a um monitor específico, nem ao HWND.
Essa parte é importante porque pode parecer estranho chamar CreateDIBSection sem passar o HWND e sem chamar GetDC antes.
Mas para esse caso não precisamos disso.
bitmap_handle = CreateDIBSection(NULL,
&bmi,
DIB_RGB_COLORS,
&bitmap_memory,
NULL,
0);
O primeiro parâmetro é um HDC. Estamos passando NULL porque queremos criar um bitmap independente usando as informações do BITMAPINFO.
Como estamos usando:
biBitCount = 32;biCompression = BI_RGB;DIB_RGB_COLORS;
o Windows tem informação suficiente para criar a memória de pixels sem depender do formato de uma janela.
O parâmetro HDC existe principalmente para casos em que o formato depende do dispositivo, paletas antigas, gerenciamento de cores e outros cenários onde o contexto de desenho influencia a criação do bitmap.
Para um back buffer simples de jogo, em 32 bits, ele pode ficar como NULL.
Visualmente, podemos pensar assim:
HBITMAP
|
v
+----------------+
| Pixel Memory |
| |
| 0x00RRGGBB |
| 0x00RRGGBB |
| 0x00RRGGBB |
+----------------+
O CreateDIBSection devolve duas coisas importantes:
- um
HBITMAP, que é o handle usado pelo GDI; - um ponteiro em
bitmap_memory, que é onde podemos escrever os pixels diretamente.
Essa combinação é exatamente o que queremos para renderização por software.
O nosso código escreve em memória normal, usando ponteiro. Depois o Windows consegue usar o HBITMAP e o BITMAPINFO para entender como copiar esses pixels para a janela.
O que é HBITMAP
HBITMAP é um handle para um objeto de bitmap do GDI.
Um handle não é o objeto em si. Ele é um identificador opaco que o Windows devolve para o programa. Com esse identificador, podemos pedir para a API operar naquele recurso.
Por exemplo:
HBITMAP bitmap_handle = CreateDIBSection(...);
O tipo HBITMAP não deve ser tratado como um ponteiro para os pixels. Os pixels estão no bitmap_memory.
Essa diferença é essencial:
HBITMAP: identifica o objeto de bitmap para o GDI;bitmap_memory: aponta para a memória real dos pixels.
Então, para desenhar manualmente, escrevemos em bitmap_memory. Para liberar o recurso do GDI, usamos DeleteObject(bitmap_handle).
if (bitmap_handle) {
DeleteObject(bitmap_handle);
bitmap_handle = NULL;
bitmap_memory = NULL;
}
Após chamar DeleteObject, o ponteiro bitmap_memory não deve mais ser usado. Ele apontava para a memória associada ao objeto destruído.
Por isso eu limpo os dois valores.
HBITMAP, HMODULE, HWND, HDC e outros handles
A Win32 API usa muitos tipos que começam com H.
Esse H normalmente significa handle.
Alguns exemplos comuns:
HWND: handle para uma janela;HDC: handle para um Device Context;HBITMAP: handle para um bitmap do GDI;HMODULE: handle para um módulo carregado no processo;HINSTANCE: historicamente representa uma instância da aplicação, hoje costuma ser equivalente a umHMODULEem muitos contextos.
Apesar de todos serem handles, eles não representam a mesma coisa.
Um HWND identifica uma janela. Você usa esse handle com funções como GetClientRect, DestroyWindow ou GetDC.
Um HDC identifica um contexto de desenho. O GDI desenha através de um DC. Ele guarda o estado de desenho: superfície alvo, brush, pen, fonte, região de clipping e outras configurações.
Um HBITMAP identifica um bitmap. Ele pode ser selecionado em um DC de memória ou usado em operações de cópia, dependendo da função.
Um HMODULE identifica um módulo carregado no processo, como o próprio executável ou uma DLL. Ele é usado para encontrar recursos, endereços de funções e informações do módulo.
Portanto, mesmo que por baixo dos panos muitos handles sejam implementados como ponteiros ou valores numéricos, você não deve misturá-los.
Isto estaria conceitualmente errado:
HMODULE module = (HMODULE)bitmap_handle; // errado
O compilador pode até aceitar um cast forçado, mas a API não vai interpretar corretamente. Um HBITMAP pertence ao mundo de objetos GDI. Um HMODULE pertence ao carregador de módulos do processo.
O tipo do handle comunica qual subsistema do Windows entende aquele valor.
Em resumo:
HWND -> janela HDC -> contexto de desenho HBITMAP -> bitmap do GDI HMODULE -> executável ou DLL carregada HINSTANCE -> instância/módulo da aplicação
Configurando o BITMAPINFO
A estrutura BITMAPINFO descreve como os pixels devem ser interpretados.
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = width; bmi.bmiHeader.biHeight = -height; bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB;
Vamos por partes.
biSize
O campo biSize informa ao Windows qual cabeçalho estamos usando.
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
No caso de BITMAPINFOHEADER, esse tamanho é 40 bytes.
Mas por que o Windows precisa disso?
Porque historicamente existiram várias versões de cabeçalhos de bitmap:
BITMAPCOREHEADER -> 12 bytes BITMAPINFOHEADER -> 40 bytes BITMAPV4HEADER -> 108 bytes BITMAPV5HEADER -> 124 bytes
Como a função recebe um ponteiro genérico para uma estrutura, ela precisa descobrir como interpretar os próximos campos.
O primeiro campo do cabeçalho é justamente o tamanho. Assim, APIs antigas e novas conseguem compartilhar um padrão parecido.
Esse padrão aparece em vários pontos da Win32 API: a estrutura carrega o próprio tamanho para a função saber qual versão está recebendo.
biWidth e biHeight
biWidth é a largura do bitmap em pixels.
biHeight é a altura, mas com um detalhe importante:
bmi.bmiHeader.biHeight = -height;
O valor negativo cria um bitmap top-down.
Isso significa que o primeiro pixel da memória representa o canto superior esquerdo da imagem.
Se usássemos altura positiva, o bitmap seria bottom-up. Nesse caso, a primeira linha da memória representaria a parte de baixo da imagem, que é o comportamento histórico dos bitmaps no Windows.
Para jogos e buffers manuais, top-down costuma ser mais natural, porque a maioria dos códigos trata (0, 0) como o canto superior esquerdo.
biPlanes
Este campo precisa ser 1.
bmi.bmiHeader.biPlanes = 1;
Ele existe por razões históricas. Para o nosso uso, não há decisão a tomar aqui.
biBitCount
Aqui definimos quantos bits cada pixel usa.
bmi.bmiHeader.biBitCount = 32;
Com 32 bits, cada pixel ocupa 4 bytes.
Em memória, usando BI_RGB, podemos escrever um pixel no formato:
0x00RRGGBB
Na prática, em uma máquina little-endian, esses bytes ficam armazenados na memória como:
BB GG RR 00
Isso pode confundir no começo. O valor inteiro que escrevemos no código é 0x00RRGGBB, mas a ordem dos bytes na memória depende da arquitetura.
biCompression
Para esse exemplo, usamos BI_RGB.
bmi.bmiHeader.biCompression = BI_RGB;
Apesar do nome falar em RGB, aqui ele significa que o bitmap não usa compressão.
Com 32 bits e BI_RGB, vamos escrever cada pixel diretamente como um inteiro de 32 bits.
biSizeImage
O campo biSizeImage pode ficar zerado nesse caso.
Quando usamos BI_RGB sem compressão, o Windows consegue calcular o tamanho da imagem a partir da largura, altura e bits por pixel.
Mas o conceito é simples: biSizeImage representa o tamanho do buffer de pixels em bytes.
Por exemplo, para uma janela de 800 x 600 com 32 bits por pixel:
32 bits = 4 bytes 800 * 600 * 4 = 1.920.000 bytes
Logo, esse buffer teria aproximadamente 1.9 MB.
Preenchendo os pixels
Depois que o CreateDIBSection cria a memória, podemos escrever nela.
unsigned int *memory = (unsigned int *)bitmap_memory;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
*memory = (unsigned int)(0 << 16 |
(unsigned char)y << 8 |
(unsigned char)x);
memory++;
}
}
Como cada pixel tem 32 bits, fazemos o cast para unsigned int *.
Assim, cada incremento em memory++ avança 4 bytes, ou seja, um pixel inteiro.
O valor do pixel está sendo montado com deslocamento de bits:
0 << 16 // vermelho (unsigned char)y << 8 // verde (unsigned char)x // azul
O cast para unsigned char conserva apenas um byte do valor. Na prática, cada canal fica entre 0 e 255.
Para um teste visual, isso é suficiente.
Recriando o buffer no resize
Quando a janela muda de tamanho, precisamos recriar o back buffer.
No callback da janela, podemos fazer isso em WM_SIZE.
case WM_SIZE: {
RECT rect;
GetClientRect(hwnd, &rect);
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
if (width > 0 && height > 0) {
create_back_buffer(hwnd, 0, 0, width, height);
}
} break;
Antes de criar o novo bitmap, a função destrói o anterior:
if (bitmap_handle) {
DeleteObject(bitmap_handle);
bitmap_handle = NULL;
bitmap_memory = NULL;
}
O CreateDIBSection cria um objeto GDI. Portanto, a função correta para liberar esse recurso é DeleteObject.
Não use free em bitmap_memory. Essa memória pertence ao objeto criado pelo GDI.
Exibindo o buffer na janela
Criar a DIBSection é só metade do processo.
Para enxergar os pixels, precisamos copiar o buffer para o HDC da janela. Podemos fazer isso no WM_PAINT.
internal void
update_window(HDC hdc, int x, int y, int width, int height)
{
StretchDIBits(hdc,
x, y, width, height, // dst
x, y, bmi.bmiHeader.biWidth, -bmi.bmiHeader.biHeight, // src
bitmap_memory,
&bmi,
DIB_RGB_COLORS,
SRCCOPY);
}
StretchDIBits copia os pixels de uma DIB para um HDC.
O nome começa com Stretch porque ela também pode redimensionar a imagem durante a cópia. Se o tamanho de origem e destino forem iguais, ela apenas copia.
Podemos chamar essa função durante o WM_PAINT:
case WM_PAINT: {
PAINTSTRUCT paint;
HDC hdc = BeginPaint(hwnd, &paint);
RECT client_rect;
GetClientRect(hwnd, &client_rect);
int width = client_rect.right - client_rect.left;
int height = client_rect.bottom - client_rect.top;
update_window(hdc, 0, 0, width, height);
EndPaint(hwnd, &paint);
} break;
Neste ponto, ainda estamos desenhando apenas quando o Windows pede uma repintura ou quando a janela muda de tamanho.
Mais adiante, em um jogo, o normal será:
- processar input;
- atualizar o estado do jogo;
- renderizar no back buffer;
- copiar o back buffer para a janela.
Esse ciclo acontece dentro do loop principal.
Device Context e bitmap
Um detalhe que costuma confundir: o GDI desenha através de HDC.
O HDC representa um contexto de desenho. Ele não é apenas uma janela e também não é apenas uma imagem. Ele é uma estrutura que guarda o alvo e o estado do desenho.
Quando usamos BeginPaint, recebemos um HDC associado à janela:
HDC hdc = BeginPaint(hwnd, &paint);
Quando usamos CreateCompatibleDC, criamos um DC em memória:
HDC memory_dc = CreateCompatibleDC(NULL);
Esse DC em memória pode ter um bitmap selecionado nele com SelectObject.
Para este artigo, o caminho mais direto é usar StretchDIBits com o ponteiro bitmap_memory e o BITMAPINFO. Assim não precisamos criar um DC de memória ainda.
Conclusão
Com CreateDIBSection conseguimos criar um back buffer simples para renderização por software no Windows.
O ponto principal é entender que existem duas visões do mesmo recurso:
- o
HBITMAP, usado pelo GDI como handle do bitmap; - o
bitmap_memory, usado pelo nosso código para escrever pixels diretamente.
O bitmap não pertence à janela. Ele é independente. A janela entra no processo apenas quando copiamos a memória para o HDC dela usando StretchDIBits.
Esse é o primeiro passo para desenhar o jogo manualmente. Daqui em diante, a renderização deixa de ser uma chamada como PatBlt e passa a ser uma escrita direta nos pixels.
Meu Acesso Rápido
- Buscar o tamanho da janela com GetClientRect
- Criar o back buffer com as definições do BITMAPINFO
- Gerar pixels de teste no bloco de memória
- Fazer o blit na janela com StretchDIBits