GUI: Superfícies

Ao invés de lidarmos diretamente com bitmaps, podemos usar algum nível de abstração. Chamarei essa abstração de superfícies, que são áreas retangulares contidas em buffers com tamanhos, alturas e formatos de pixel e, é claro, ponteiros para arrays contendo os pixels:

struct surface {
  uint32_t width, height; // em "pixels".
  enum pixel_format pelfmt;
  void *buffp;
  struct palette *palp;
};

O campo pelfmt pode assumir (por enquanto) os valores de PELFMT_1BPP, PELFMT_8BPP, PELFMT_INT, PELFMT_RGB e PELFMT_RGBA, onde os dois últimos implicam em 1 byte por componente…O primeiro é usado para bitmaps que tenham 1 bit por pixel e, por consequência, 8 pixels por byte; e o segundo para bitmaps indexados, de 256 cores, onde uma paleta de cores é necessária. A paleta, usada apenas neste caso, tem a estrutura:

struct palette {
  size_t numentries;
  char entries[0]; // triplets RGB para cada entrada.
};

O formato PELFMT_INT é usado, primariamente, para quando formos mexer com Z Buffering, onde precisaremos armazenar apenas a componente Z de cada pixel.

Podemos ter valores diferentes, como PELFMT_R5G6B5 para pixels de 16 bits, por exemplo, mas esses 5 iniciais, acredito, são mais que suficientes.

A ideia da superfície é manter essa área total que pode ser manipulada em sub áreas ou “regiões” (retângulos) interiores. Por exemplo:

Superfície com uma região interna

A área cinzenta corresponde a totalidade da superfície e a região é especificada, na API, através de uma simples estrutura de retângulo:

struct rect {
  int left, top;
  int right, bottom;
};

Não uso pares de coordenadas nomeadas (x0,y0) e (x1,y1) para não causar confusão… (left,top) e (right,bottom) implica que os pontos do retângulo são posicionados exatamente como mostrados na figura…

Assim, com a API que lida com essa abstração, podemos fazer cópias de regiões de uma superfície para outra usando métodos de chamados de Bit Block Transfers ou BitBlt. É bom notar que mesmo uma linha horizontal ou vertical estão contidas numa região retangular de uma superfície se usarmos o sistema de coordenadas relacionado às bordas dos pixels, como mostrei neste artigo. Mas, em que, especificamente isso tudo é útil?

O framebuffer, por exemplo, pode ser definido como uma superfície de tamanho 1024×768 pixels, com formato RGBA assim:

struct surface frontvbuffer = {
  .width = 1024,
  .height = 768,
  .pelfmt = PELFMT_RGBA,
  .buffp = (void *)0xe0000000 // pointer para o framebuffer.
};

Qualquer manipulação com essa superfície fará com que os pixels vão diretamente para a tela. E, com o conceito de regiões, podemos também ter uma lista delas que indicam áreas da superfície que foram modificadas na última atualização da tela:

struct region_item {
  struct _list_head *next;
  struct rect rc;
};

// Cria uma lista vazia de regiões.
struct _list_head region_head = INIT_LIST;

Com essa lista, toda vez que modificarmos um lugar na superfície do frontvbuffer podemos acrescentar uma região (ou expandimos uma que já existe na lista), nos informando que essa é uma das regiões que teremos que copiar de ou para algum outro lugar (back buffer?). De qualquer maneira, é mais fácil fazer transferências de blocos de regiões do que de uma superfície inteira porque temos menos bytes para manipular!

Mais adiante mostrarei a técnica de manter essa lista de regiões modificadas num troço chamado de “Dirty Rectangles“.

As superfícies são mais flexíveis do que citei acima porque podem ser usadas, por exemplo, para manter “texturas”, “stencil buffers”, “Z buffers”, etc, bastando implementar um “formato de pixel” diferente e, é claro, essas superfícies secundárias podem ser alocadas dinamicamente e dealocadas ao bel prazer… No caso de texturas, imagine o caso onde você tem uma fonte bit-mapeada. Poderia ter uma textura como abaixo dentro de uma superfície:

Essa superfície pode ser dividida, claramente, em várias regiões. Cada “região” mapeada para um índice correspondente ao caractere ASCII que você quer desenhar numa outra superfície. Basta achar a região desejada e fazer um BitBlt dessa superfície para uma posição (x,y) desejada na superfície onde o caractere será desenhado (no frontvbuffer, por exemplo)…

A implementação do gerenciamento de regiões e as rotinas de BitBlt eu deixarei para outro artigo, mas considere que temos agora um container único para todo o tipo de gráficos que quisermos manipular e teremos meios unificados e rápidos para fazê-lo… As funções BitBlt levam em conta coisas interessantes, como o comprimento de uma scanline da região (podemos também usar registradores maiores para mover mais dados de uma tacada só) e se a região encontra-se totalmente dentro da superfície. Considere esse caso:

Parte da região está fora da superfície

Neste caso a parte mais “escura” da região poderá ser copiada e a parte mais clara será, obviamente, totalmente transparente ou ignorada… Esse tipo de cópia de região entre superfícies fica simples de ser feita, mas temos dois casos mais complicados. Considere este outro:

O lado esquerdo deverá ser transparente

Claro que o lado esquerdo da região será transparente, mas, quando ela for copiada para outra superfície, os pixels na área mais “verde-escura” estarão deslocados no mesmo tanto da quantidade de pixels transparentes (na área “verde-clara”), na superfície destino! A mesma coisa acontece se a região sendo copiada for maior do que a área da superfície inteira, em qualquer sentido (x ou y)…

ATENÇÃO!!!

É importante lembrar que essa “superfície” é uma abstração. É uma área retangular limitante onde podemos manipular “regiões” retangulares de seu interior: Preenchendo-as e copiando-as para outras regiões ou outras superfícies. E as rotinas que farão isso precisam levar em conta os casos especiais (como mostrei acima) e outros até: O que acontece se quisermos copiar uma região de uma superfície que tem um retângulo menor que o retângulo para o qual vamos copiar na superfície destino? Que tipo de filtragem faremos? Linear? Cúbica? Bi-Cubica? A mesma coisa se for o contrário (região fonte maior que a região destino)?

Existem ainda detalhes que podemos implementar usando a analogia da superfície. Por exemplo, multisampling, onde um pixel real é composto, na verdade, por muitos pixels na superfície… Isso permitirá alguma interpolação para implementar melhor antialiasing, mas consumirá alguma memória adicional e processamento.

De qualquer maneira, a junção do conceito de superfícies, regiões e do sistema de coordenadas que mostrei anteriormente forma uma ferramenta simples, mas poderosa para lidarmos com gráficos…