Double buffering sem paginação (dirty rectangles)

Double Buffering é a técnica onde temos dois buffers na memória de vídeo onde desenha-se no buffer “invisível” e muda-se o ponteiro do início do framebuffer para esse buffer, quando terminarmos, tornando o outro buffer (que estava “na frente”) o novo back buffer… É claro que existe a necessidade de “sincronizar” os buffers, copiando as modificações feitas no novo front buffer para o novo back buffer, para que os novos desenhos reflitam as modificações feitas…

Acontece que tem gente que está lidando com os modos gráficos VESA, em modo protegido, sem o auxílio da Protected Mode Interface, que não está disponível em máquinas virtuais como QEMU e VirtualBox (e, suspeito, também não exista no VMWare)… Nesse caso, esse esquema de double buffering não é lá muito útil, em termos de performance. A solução? “Retângulos Sujos” (dirty rectangles).

A técnica leva em consideração que podemos fazer BitBlt apenas das regiões que foram modificadas na back surface para a front surface. Essas superfícies podem ser mapeadas assim: A front surface contém, no seu ponteiro para o buffer, o endereço do linear framebuffer (0xE0000000, no QEMU, por exemplo) e a back surface pode ser alocada na memória do sistema (RAM). O estado inicial de ambas as superfícies devem ser idênticas e podemos conseguir isso “limpando” os buffers (preenchendo com zeros).

Daí para frente, quaisquer modificações na back surface cria regiões… Por exemplo, Se traçarmos uma linha de (x_0,y_0) para (x_1, y_1), cria-se um retângulo \{x_0,y_0,x_1+1,y_1+1\} (lembre-se do sistema de coordenadas que estamos usando!) e o colocamos (apenas suas coordenadas) numa lista… Claro, desenharemos, na back surface a linha desejada… Todas as outras modificações no back buffer acrescentarão coordenadas de retângulos na lista. Na hora de copiar as modificações feitas na back surface para a front surface teremos que fazer apenas duas coisas:

  1. Esperar pelo retraço vertical para não causar flickering;
  2. Percorrer a lista de retângulos, fazendo BitBltCopy da back surface para a front surface.

A espera pelo retraço vertical é simples… Embora os modos de vídeo não sejam padrão VGA, toda e qualquer placa de vídeo, inclusive as emuladas, suportam os controladores do padrão VGA. Retraço vertical é uma coisa simples de verificar:

inline unsigned char inb(unsigned short port)
{
  unsigned char result;

  __asm__ __volatile__ (
    "in %1,%b0" 
    : "=a" (result) : "dN" (port)
  );

  return result;
}

inline int vsync(void)
{
  // Se o bit 3 do registrador "VGA Input Status" for 1,
  // estamos no sincronismo vertical!
  return !!(inb(0x3da) & (1 << 3));
}

A lista de retângulos também é fácil de manter (veja este artigo aqui):

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

// "Cabeça da lista"...
// Os itens que serão adicionados na lista são do tipo
// struct dirtyrect_entry, acima.
struct _list_head backsurf_dirtyrec_list =
  EMPTY_LIST(backsurf_dirtyrec_list);

Adicionar retângulos na lista é só uma questão de usar a função _list_add_after ou _list_add_tail, de acordo com suas necessidades… Uma vez que todos os retângulos “sujos” tenham sido adicionados à lista, a cópia para a front surface é tão simples quanto:

struct _list_head *p;
foreach(p, backsurf_dirtyrec_list)
{
  struct rect *rc = &((struct dirtyrect_entry *)p)->rc;

  BitBltCopy(frontsurfp, 
             rc->left, rc->top,
             backsurfp,
             rc);
}

Daí podemos nos livrar de todos os itens da lista e começar tudo de novo… Claro, se você fizer com carinho, pode retirar o item da lista no próprio loop acima…

O uso de transferências de blocos usando BitBltCopy tem a vantagem de que o recorte dos retângulos será “automático” (veja o artigo anterior), mas o ponto importante aqui é que, na back surface, devemos lidar apenas com regiões pequenas e/ou com a menor quantidade possível de regiões… Isso implica que se uma região já esteja na lista, ou parte dela, podemos simplesmente estender o retângulo ou deixá-lo como está… Por exemplo: Suponha que você traçou uma linha de (10,10) até (20,20). Isso te dará um retângulo de coordenadas (10,10,21,21) e ele é colocado na lista… Logo depois você traça outra linha de (19,18) para (11,11). O que fazer?

O primeiro passo é varrer a lista para ver se já existe um retângulo onde essas coordenadas se encaixam, senão, criar um retângulo (11,11,20,19) e colocá-lo na lista. No exemplo acima, já temos o retânculo (10,10,21,21) e, portanto, já que o novo retângulo encontra-se “dentro” deste, nenhum retângulo adicional é necessário.

Existe o caso, também, de termos um retângulo (10,10,21,21) na lista e queremos colocar outro de coordenadas (5,5,12,12). Neste caso, como há uma sobreposição de retângulos, podemos modificar o de coordenadas (10,10,21,21) para (5,5,21,21), sem adicionar nenhum retângulo novo.

Como você fará para decidir se o retângulo adicionado é grande demais ou pequeno demais, fica a cargo da sua rotina de gerência de retângulos. A ideia é apenas copiar blocos menores que uma superfície inteira para outra…

Anúncios