Placa de vídeo

VBE, memória mapeada, cores, XPM, sprites e apresentação de frames.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

O píxel (3,1) não tem necessariamente o mesmo endereço em dois modos gráficos. A largura da imagem, os bytes por píxel e o espaço entre linhas determinam onde escrever. O framebuffer guarda as cores; o controlador de vídeo lê essa memória para apresentar a imagem.

Modo e informação VBE

A VBE, Video BIOS Extension, oferece uma interface para consultar e selecionar modos gráficos sem programar os registos privados de cada GPU. O ambiente do laboratório usa a interface da BIOS INT 0x10, invocada através do apoio Minix.

Função em AXOperação
0x4F00Informação do controlador VBE
0x4F01Informação de um modo
0x4F02Selecionar modo
0x4F07Consultar ou alterar o início da imagem apresentada

Para selecionar o modo 0x105 com framebuffer linear, BX recebe 0x105 | (1 << 14), ou 0x4105. O bit 14 pede o acesso linear, ou seja, não faz parte do identificador base do modo.

Uma função completa para o ambiente Minix/LCF, com o apoio de BIOS da imagem da cadeira, é:

#include <lcom/lcf.h>
#include <machine/int86.h>
#include <stdint.h>

int set_linear_mode(uint16_t mode) {
  reg86_t registers = {0};
  registers.intno = 0x10;
  registers.ax = 0x4F02;
  registers.bx = (uint16_t)(mode | (1u << 14));
  int result = sys_int86(&registers);
  if (result != OK) return result;
  return registers.ax == 0x004F ? OK : 1;
}

O resultado esperado de set_linear_mode(0x105) é o modo selecionado, se suportado. A função verifica duas coisas: a chamada Minix foi executada e a VBE devolveu sucesso em AX. Não aloca nem mapeia o framebuffer. Não funciona num compilador C portátil nem substitui a inicialização completa de vídeo.

Antes de desenhar, consulta a informação do modo, porque não podes assumir que dois modos de 16 bpp têm os mesmos campos de cor. Conserva resolução horizontal e vertical, bits por píxel, bytes por linha, modelo de memória, posições e tamanhos dos campos RGB e endereço físico da VRAM.

A função VBE 0x4F01 escreve um bloco de informação em memória acessível pela BIOS, indicado por ES:DI no modelo de real mode. O apoio da cadeira trata dessa alocação e cópia para vbe_mode_info_t. Um apontador virtual arbitrário de C não é automaticamente um endereço válido para a BIOS.

Mapear memória física

PhysBasePtr é o endereço físico do framebuffer, por isso não o convertas simplesmente para um apontador e escrevas: o processo usa endereços virtuais. No Minix, a sequência é obter permissão para a região com sys_privctl, mapeá-la com vm_map_phys e conservar o apontador virtual devolvido.

Se cada linha ocupa pitch bytes e há height linhas, a região usada por uma imagem ocupa pitch * height bytes. Valida o produto num tipo capaz de representar o tamanho e confere falhas em todas as operações. Ao sair, desmonta o mapeamento e repõe o modo de texto através do apoio do ambiente. Falhar depois de selecionar o modo também exige reposição.

Calcular o endereço de um píxel

Num modo linear com píxeis ocupando um número inteiro de bytes:

B=⌈bpp8⌉,offset(x,y)=y⋅pitch+x⋅B.B=\left\lceil\frac{bpp}{8}\right\rceil,\qquad \text{offset}(x,y)=y\cdot pitch+x\cdot B.

BB é o número de bytes por píxel. pitch pode ser maior do que width * B, devido a alinhamento ou organização do modo. O intervalo visível exige 0≤x<width0\leq x<width e 0≤y<height0\leq y<height.

No modo de 1024 por 768 a 8 bpp com pitch = 1024, o píxel (200,150) começa em 150⋅1024+200=153800150\cdot1024+200=153800. Uma imagem ocupa 786432 bytes, ou 768 KiB. Isso não é necessariamente toda a VRAM disponível na placa.

Agora considera largura 4, altura 2, 24 bpp e pitch = 14. Cada linha tem 12 bytes visíveis mais dois bytes de padding. O píxel (3,1) começa em 14+3⋅3=2314+3\cdot3=23 e ocupa os bytes 23, 24 e 25. O cálculo y * width * 3 + x * 3 começaria erradamente no byte 21.

Na figura, a segunda linha começa em 14, não em 12; os três bytes destacados são o píxel pedido e os dois últimos de cada linha ficam por usar.

Índices e cores diretas

Num modo indexado, o valor do píxel escolhe uma entrada da paleta. Com oito bits, há até 256 índices; isso não significa que cada índice seja um valor RGB de oito bits.

Num modo direct color, a palavra do píxel contém componentes. Para cada componente com ss bits a partir da posição pp, isola-a com:

component=(color≫p)&(2s−1).component=(color\gg p)\mathbin{\&}(2^s-1).

Para compor a cor, limita cada componente ao seu campo e desloca-a para a posição correta. No formato RGB565, R tem cinco bits na posição 11, G seis na posição 5 e B cinco na posição 0. Para (R,G,B)=(31,0,15)(R,G,B)=(31,0,15):

color=(31≪11) ∣ (0≪5) ∣ 15=0xF80F.color=(31\ll11)\,|\,(0\ll5)\,|\,15=\texttt{0xF80F}.

No x86 little-endian, os dois bytes em memória são 0F F8. O significado dos campos da palavra e a ordem dos bytes em memória são questões diferentes. Em 24 bpp, não existe um tipo uint24_t padrão e escrever um uint32_t pisaria o píxel seguinte.

Este programa portátil escreve um píxel de 24 bits numa imagem com padding e imprime 56 34 12, porque guarda os três bytes em ordem little-endian. Define explicitamente que os três bytes contêm os 24 bits inferiores:

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

static bool pixel24(uint8_t *buffer, size_t size, size_t pitch,
                    size_t width, size_t height, size_t x, size_t y,
                    uint32_t color) {
  if (x >= width || y >= height) return false;
  size_t offset = y * pitch + x * 3;
  if (offset > size || size - offset < 3) return false;
  for (unsigned i = 0; i < 3; ++i)
    buffer[offset + i] = (uint8_t)(color >> (8 * i));
  return true;
}

int main(void) {
  uint8_t image[28] = {0};
  if (!pixel24(image, sizeof image, 14, 4, 2, 3, 1, 0x123456))
    return 1;
  printf("%02X %02X %02X\n", (unsigned)image[23],
         (unsigned)image[24], (unsigned)image[25]);
  return 0;
}

O exemplo guarda os 24 bits inferiores da palavra. Não converte componentes RGB para campos arbitrários de um modo VBE. As dimensões fixas são pequenas e os produtos não transbordam. Uma API que aceite dimensões arbitrárias deve validar também as multiplicações.

Linhas, retângulos e clipping

Uma linha horizontal escreve píxeis com X consecutivo e Y fixo. Um retângulo chama a mesma operação em várias linhas. Para largura ww e altura hh, desenha no intervalo semiaberto [x,x+w)×[y,y+h)[x,x+w)\times[y,y+h), que contém whwh píxeis.

Escolhe o contrato: rejeitar figuras parcialmente fora do ecrã ou recortá-las, clipping. Para rejeitar sem overflow, depois de validar a origem, compara w <= screen_width - x e h <= screen_height - y. Calcular x + w primeiro num tipo pequeno pode dar a volta e aceitar uma figura inválida.

Para copiar uma linha de um sprite, respeita o passo entre linhas tanto na origem como no destino. Uma única cópia de sprite_width * sprite_height * B só é correta se a disposição dos dois retângulos for contígua de forma compatível. No ecrã, a próxima linha pode começar muito depois do fim da linha do sprite.

Padrão de cores por retângulos

Para desenhar uma grelha de n por n retângulos, escolhe n positivo, sem exceder a largura nem a altura do ecrã. Separa a posição na grelha da posição em píxeis. A coluna col determina X e a linha row determina Y: com largura de célula cw e altura ch, o retângulo (col, row) começa em (col * cw, row * ch). Se cada retângulo tiver largura width / n e altura height / n, as divisões inteiras podem deixar faixas no bordo; define se as conservas ou as distribuis pelo último retângulo.

Num modo indexado, um padrão pode usar:

index(row,col)=(first+(row⋅n+col)⋅step) mod 2bpp.index(row,col)=(first+(row\cdot n+col)\cdot step)\bmod 2^{bpp}.

O resto mantém o índice dentro da largura do píxel; a paleta decide a cor desse índice. Num modo de cor direta, varia cada componente no seu próprio intervalo. Por exemplo, num padrão RGB565 com componentes iniciais (30,62,29)(30,62,29) e step = 3, o retângulo da linha 1 e coluna 2 pode usar:

R=(30+2⋅3) mod 32=4,R=(30+2\cdot3)\bmod32=4, G=(62+1⋅3) mod 64=1,G=(62+1\cdot3)\bmod64=1, B=(29+(1+2)⋅3) mod 32=6.B=(29+(1+2)\cdot3)\bmod32=6.

A cor composta é (4 << 11) | (1 << 5) | 6, ou 0x2026. O módulo de cada campo depende do seu número de bits, não dos 16 bits do píxel completo. Estas fórmulas definem este padrão; não são uma exigência da VBE.

XPM e sprites

Um XPM representa píxeis por símbolos de texto. O cabeçalho indica largura, altura, número de cores e número de caracteres por píxel. Seguem-se as definições de cores e as linhas da imagem. Um XPM de altura hh com cc cores contém, no formato simples usado no exemplo, 1+c+h1+c+h strings.

xpm_load converte essa representação para um pixmap no formato pedido, como XPM_INDEXED ou XPM_8_8_8. A estrutura xpm_image_t guarda dimensões, tamanho e bytes. Verifica se a conversão devolve NULL. Carrega uma vez por recurso e reutiliza o resultado, em vez de voltar a interpretar o XPM em cada frame.

Um sprite reúne um pixmap e a informação necessária para o colocar na cena. A transparência significa não escrever certos píxeis do sprite. Não assumas que preto é sempre transparente: usa o valor e o formato definidos para o recurso. A ordem de desenho determina quais objetos ficam por cima de outros.

Uma animação pode escolher um de vários pixmaps. Se cada figura dura quatro frames e desenhas a 30 frames/s, cada figura dura 4/304/30 s. Com três figuras, um ciclo completo dura 12/30=0,412/30=0{,}4 s.

Para movimento, se speed = 3 significa três píxeis por frame, a 30 frames/s obtemos 90 píxeis/s. Na convenção de velocidade negativa usada em exercícios do laboratório, speed = -3 significa um píxel a cada três frames, ou 10 píxeis/s a 30 frames/s. Limita o último passo à distância que falta, para chegar à posição final sem passar além dela.

Double buffering e tearing

Desenhar diretamente na imagem visível pode mostrar um fundo apagado ou objetos incompletos. Com double buffering, compões o frame completo num back buffer e depois copias para o framebuffer. Isso reduz a exposição das fases intermédias de desenho, mas uma cópia ainda demora tempo.

Se o controlador ler o framebuffer durante a cópia, uma parte da imagem pode ser do frame antigo e outra do novo. Isso é tearing. Double buffering por cópia não garante que o ecrã veja apenas frames completos.

Cópia a meio do refrescamento

O controlador leu partes de dois frames. A fronteira é o tearing.

Troca no intervalo vertical

A página nova estava completa antes da troca e fica estável durante a leitura.

Quatro linhas ilustram a leitura de uma imagem. O back buffer pode estar completo nos dois casos; a diferença está no instante da apresentação.

Com page flipping, ambos os buffers estão na VRAM e alteras qual é o apresentado, usando uma operação como VBE Set Display Start. Para evitar tearing, a mudança deve respeitar a sincronização com o refrescamento vertical. A frequência do timer da aplicação não garante que estejas sincronizado com o ecrã.

Uma cópia sincronizada também tem de caber no intervalo permitido pelo modo. Uma troca de página agendada para o refrescamento evita copiar todos os píxeis nesse intervalo, se a implementação suportar essa operação.

Triple buffering

Triple buffering acrescenta outro back buffer para preparar um novo frame enquanto um frame completo aguarda apresentação. Exige uma política sobre qual apresentar e o apoio da implementação. Distingue sempre tempo de simulação, ritmo de construção dos frames e ritmo de refrescamento do ecrã.

Exercícios

Calcular offset com padding

Uma imagem linear tem largura 8, altura 4, 24 bpp e pitch de 28 bytes. Qual é o offset inicial do píxel (3,2)?

Pista
24 bpp ocupa três bytes por píxel.
Solução

Offset = 2 × 28 + 3 × 3 = 65 bytes. O píxel ocupa os bytes 65, 66 e 67; o início da linha seguinte continua a depender do pitch.

Erros frequentes

Usar a largura visível em vez de pitch ou somar x sem o multiplicar por bytes por píxel.

Voltar à explicação
Interpretar velocidade negativa

Um sprite percorre 600 píxeis. Desenhas a 30 frames/s e speed = -3 significa um píxel a cada três frames. Quanto dura o movimento, ignorando a apresentação inicial?

Pista
Cada píxel precisa de três frames.
Solução

São 1800 frames e 1800/30 = 60 segundos. A velocidade média é 10 píxeis/s. O sinal negativo representa frames por píxel nesta convenção, não movimento na direção contrária.

Erros frequentes

Tratar -3 como três píxeis por frame ou usar a frequência do timer quando a frequência de desenho é diferente.

Voltar à explicação
Avaliar double buffering

Compões um frame inteiro num back buffer e copias para a VRAM sem sincronização. Isso garante ausência de tearing? Descreve um instante em que o ecrã ainda pode mostrar duas imagens.

Pista
A cópia ocupa um intervalo de tempo.
Mais uma pista

O controlador pode ler uma zona já copiada e outra ainda antiga.

Solução

Não garante. Durante a cópia, o controlador pode apresentar linhas com o frame novo e linhas ainda do antigo. O back buffer evita mostrar as etapas do desenho, mas a apresentação precisa de sincronização adequada. Page flipping também pode rasgar a imagem se mudar a página a meio da leitura.

Confere a tua resposta:

  • Distingui desenho do back buffer e cópia.
  • Expliquei leitura simultânea pelo controlador.
  • Relacionei apresentação com sincronização vertical.

Erros frequentes

Dizer que memcpy é instantâneo ou confundir frequência de timer com sincronização do refrescamento.

Voltar à explicação
Isolar componentes de uma cor

No formato RGB565, a palavra de cor é 0xB7D4. Qual é a componente verde, entre 0 e 63?

Pista
Verde ocupa seis bits a partir da posição 5.
Solução

(0xB7D4 >> 5) & 0x3F = 62. Os campos vermelho e azul dão 22 e 20. Com a máscara errada 0x1F obterias 30, não 62, por isso este exemplo distingue os seis bits do verde. A ordem little-endian dos bytes em memória não muda estas posições na palavra.

Erros frequentes

Usar máscara 0x1F perde o sexto bit verde; trocar os bytes antes de isolar um campo altera a palavra original.

Voltar à explicação
Transparência e ordem dos sprites

Uma linha do fundo contém [1, 2, 3]. Os sprites A e B cobrem as mesmas três posições: A contém [T, 0, 9] e B contém [T, 6, T]. T é o valor transparente do recurso; 0 é preto visível.

Dá a linha depois de desenhar A e depois B sobre o fundo. O que muda se desenhares B antes de A? Explica que píxeis saltas.

Pista
Um píxel transparente conserva o valor que já estava no destino.
Solução

A conserva a primeira posição, escreve preto na segunda e 9 na terceira: [1, 0, 9]. B conserva a primeira e a terceira, mas substitui a segunda por 6. O resultado é [1, 6, 9].

Na ordem inversa, B produz [1, 6, 3] e A produz [1, 0, 9]. Só saltas T; saltar também o preto impediria A de ocultar o 6 na segunda posição. A ordem de desenho determina o sprite visível onde ambos escrevem.

Confere a tua resposta:

  • Obtive [1, 0, 9] depois de desenhar A.
  • Obtive [1, 6, 9] depois de desenhar B.
  • Conservei o preto visível e expliquei a diferença ao inverter a ordem.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.