Y2S2-LabComputadores

Video Card

Tópicos

Video Modes

O Minix possui dois modos de funcionamento:

Modos do Minix


Para utilizar o modo gráfico é necessário configurá-lo através da Video Bios Extension (VBE). Este método usa duas novas ferramentas:

A estrutura reg86_t possui alguns parâmetros de 16 bits cujo valor depende da configuração:

| Parâmetro reg86_t | Text Mode | Graphic Mode | |-------------------|-----------|--------------------| | ah | 0x00 | 0x4F | | al | Ox03 | 0x02 | | ax | 0x0003 | 0x4F02 | | bx | 0x0000 | submode \| BIT(14) | | intno | 0x10 | 0x10 |

AX pode ser interpretado como dois parâmetros de 8 bits: o AH (higher bits) e AL (lower bits), sendo AL responsável por determinar o modo de funcionamento (0x02 ou 0x03). BX representa o submodo de funcionamento no modo gráfico. BX também possui o BIT 14 ativo para tornar o mapeamento da memória linear. O parâmetro “intno” é 0x10 nos dois casos.

Dado um submodo de funcionamento, a mudança para o respectivo modo gráfico pode ser realizada recorrendo à seguinte implementação:

int set_graphic_mode(uint16_t submode) {
    reg86_t reg86;
    memset(&reg86, 0, sizeof(reg86)); // inicialização da estrutura com o valor 0 em todos os parâmetros
    reg86.intno = 0x10;               // intno é sempre 0x10      
    reg86.ah = 0x4F;                  // parte mais significativa de AX
    reg86.al = 0x02;                  // parte menos significativa de AX. 0x02 no caso de modo gráfico
    // reg86.ax = 0x4F02;             // equivamente às duas últimas instruções
    reg86.bx = submode | BIT(14);     // determinação do submodo com memória linear
    if (sys_int86(&reg86) != 0) {     // se houver algum erro, abortar a função
        printf("Set graphic mode failed\n");
        return 1;
    }
    return 0;
}

Tal como nos labs anteriores é importante deixar o Minix no mesmo estado por motivos de integridade do sistema. Assim é necessário voltar ao modo de texto antes de retornar qualquer função que use o modo gráfico. Uma possível solução:

int set_text_mode() {
    reg86_t reg86;                       
    memset(&reg86, 0, sizeof(reg86));   // inicialização da estrutura com o valor 0 em todos os parâmetros
    reg86.intno = 0x10;                 // intno é sempre 0x10 
    reg86.ah = 0x00;                    // parte mais significativa de AX 
    reg86.al = 0x03;                    // parte menos significativa de AX. 0x03 no caso de modo texto
    // reg86.ax = 0x0003;               // equivamente às duas últimas instruções
    reg86.bx = 0x0000;                  // não há submodo no modo de texto
    if(sys_int86(&reg86) != 0) {        // se houver algum erro, abortar a função
        printf("Set text mode failed\n");
        return 1;
    }
    return 0;
}

A LCF já possui uma implementação interna desta função: vg_exit().

Mapeamento da Video RAM

A VRAM (Video RAM) é a memória que contém informação sobre a cor de cada píxel presente no ecrã. Antes de mudar o Minix para o modo gráfico é importante alocar memória suficiente de acordo com o submodo escolhido. Ao conjunto de memória alocada dá-se o nome de frame buffer.

Frame Buffer


A VRAM no modo gráfico é sempre linear e contínua, ou seja, a matriz de cores é na realidade um array contínuo de bytes. O seu tamanho depende:

Mode Screen Resolution Color Model Bits per pixel (R:G:B)
0x105 1024x768 Indexed 8
0x110 640x480 Direct color 15((1:)5:5:5)
0x115 800x600 Direct color 24 (8:8:8)
0x11A 1280x1024 Direct color 16 (5:6:5)
0x14C 1152x864 Direct color 32 ((8:)8:8:8)

Conhecendo HRES (resolução horizontal) é possível encontrar o índice do píxel que corresponde às coordenadas 2D do frame buffer:

uint32_t pixel_index(uint16_t x, uint16_t y) {
    return y * HRES + x;
}

Conhecendo BitsPerPixel (bits por pixel de cada cor, valor que varia com o modo) é possível encontrar a zona de memória a modificar. Como os offsets na memória são sempre em bytes precisamos primeiro de converter o número de bits em bytes usando um arredondamento por excesso. Por exemplo, no modo 0x110 precisa de 15 bits, que equivale na realidade a 2 bytes, por muito que não sejam totalmente preenchidos:

uint32_t frame_buffer_index(uint16_t x, uint16_t y) {
    uint8_t bytes_per_pixel = (BitsPerPixel + 7) / 8; // Arredondamento por excesso
    return pixel_index(x, y) * bytes_per_pixel;       // Zona de memória onde começa o pixel (x, y)   
}

As cores podem vir em dois formatos:

Paleta de cores indexadas do Minix


Nesta versão do Minix estão disponíveis 64 cores indexadas. Sabendo as características de cada modo é simples calcular o número de bytes de memória a alocar. Por exemplo:

unsigned int frame_0x105_bytes = 1024 x 768  x 1; // 8  bits = 1 byte
unsigned int frame_0x11A_bytes = 1280 x 1024 x 2; // 16 bits = 2 bytes

A LCF (LCOM Framework) já possui uma função que retorna os dados de cada modo e uma estrutura que comporta toda essa informação:

vbe_mode_info_t mode_info;                          // declaração da estrutura
memset(&mode_info, 0, sizeof(mode_info));           // inicialmente todos os valores são 0
if (vbe_get_mode_info(mode, &mode_info)) return 1;  // preenchimento dos valores

Os parâmetros importantes de vbe_mode_info_t são os seguintes:

Onde <COLOR> pode uma qualquer cor primária do sistema RGB: Red, Green ou Blue.

A LCF também possui uma estrutura minix_mem_range que suporta o endereço físico base e o endereço físico final do frame buffer a alocar e system calls que transformam esses endereços de memória física em memória virtual. De acordo com a unidade curricular de Sistemas Operativos um programa só deve lidar com endereços de memória virtual.

/* variáveis globais no ficheiro graphic.c */

// estrutura de dados que contém informação sobre o modo gráfico
vbe_mode_info_t mode_info; 
// apontador para o início da memória virtual 
uint8_t *frame_buffer;

/* função de mapeamento da VRAM */
int set_frame_buffer(uint16_t mode){

  // retirar informação sobre o @mode
  memset(&mode_info, 0, sizeof(mode_info));
  if(vbe_get_mode_info(mode, &mode_info)) return 1;

  // cálculo do tamanho do frame buffer, em bytes
  uint8_t bytes_per_pixel = (BitsPerPixel + 7) / 8;
  unsigned int frame_size = mode_info.XResolution * mode_info.YResolution * bytes_per_pixel;
  
  // preenchimento dos endereços físicos
  struct minix_mem_range physic_addresses;
  physic_addresses.mr_base = mode_info.PhysBasePtr; // início físico do buffer
  physic_addresses.mr_limit = physic_addresses.mr_base + frame_size; // fim físico do buffer
  
  // alocação física da memória necessária para o frame buffer
  if (sys_privctl(SELF, SYS_PRIV_ADD_MEM, &physic_addresses)) {
    printf("Physical memory allocation error\n");
    return 1;
  }

  // alocação virtual da memória necessária para o frame buffer
  frame_buffer = vm_map_phys(SELF, (void*) physic_addresses.mr_base, frame_size);
  if (frame_buffer == NULL) {
    printf("Virtual memory allocation error");
    return 1;
  }

  return 0;
}

Com o frame buffer instanciado, podemos inserir qualquer cor em qualquer posição:

int paint_pixel(uint16_t x, uint16_t y, uint32_t color) {

  // As coordenadas têm de ser válidas
  if(x >= mode_info.XResolution || y >= mode_info.YResolution) return 1;
  
  // Cálculo dos Bytes per pixel da cor escolhida. Arredondamento por excesso.
  unsigned BytesPerPixel = (mode_info.BitsPerPixel + 7) / 8;

  // Índice (em bytes) da zona do píxel a colorir
  unsigned int index = (mode_info.XResolution * y + x) * BytesPerPixel;

  // A partir da zona de memória frame_buffer[index], copia @BytesPerPixel bytes da @color
  if (memcpy(&frame_buffer[index], &color, BytesPerPixel) == NULL) return 1;

  return 0;
}

Confesso que às vezes os cálculos, arredondamentos e offsets podem ser confusos. Em 2023 criei uma forma mais interativa de ver o que acontece, que está dispoível aqui.

Interactive Frame Buffer

Uma vez que esta folha é read-only, podem inspecionar as fórmulas, assim como copiar e colar para um sítio que possam editar.

XPM

X PixMap (XPM) é uma forma de representação de imagens. O ficheiro fornecido pixmap.h possui alguns exemplos no formato interpretado pelo Minix. Exemplo:

static xpm_row_t const pic1[] = {
  "32 13 4",
  "  0",
  "x 2",
  "o 14",
  "+ 4",
  "                                ",
  "              xxx               ",
  "            xxxxxxx             ",
  "         xxxxxx+xxxxxx          ",
  "      xxxxxxx+++++xxxxxxx       ",
  "    xxxxxxx+++++++++xxxxxxx     ",
  "    xxxxxxx+++++++++xxxxxxx     ",
  "      xxxxxxx+++++xxxxxxx       ",
  "         xxxxxx+xxxxxx          ",
  "          ooxxxxxxxoo           ",
  "       ooo           ooo        ",
  "     ooo               ooo      ",
  "   ooo                   ooo    "};

A primeira linha revela, por ordem:

Em seguida há a descrição das cores. Cada linha possui dois ou três argumentos:

A LCF providencia funções e estruturas de dados para lidar com XPMs:

uint8_t *xpm_load(xpm_map_t map, enum xpm_image_type type, xpm_image_t *img);

A descrição e tipo dos argumentos podem ser encontrados aqui. Assim é possível criar uma função que imprime em modo gráfico uma imagem XPM:

int print_xpm(xpm_map_t xpm, uint16_t x, uint16_t y) {

  xpm_image_t img;

  // retorna um apontador para um array de cores preenchido de acordo com o XPM
  // atualiza @img com o valor do comprimento e altura da imagem
  uint8_t *colors = xpm_load(xpm, XPM_INDEXED, &img);
  if (colors == NULL) return 1;

  for (int h = 0 ; h < img.height ; h++) {
    for (int w = 0 ; w < img.width ; w++) {
      if (vg_draw_pixel(x + w, y + h, *colors) != 0) return 1;
      colors++; // avança para a próxima cor
    }
  }
  return 0;
}

Compilação do código

Ao longo do Lab5 programamos em 2 ficheiros:

Ainda importamos os ficheiros utils.c, timer.c, i8254.h, i8042.h, KBC.c e keyboard.c dos labs anteriores. O ficheiro pixmap.h também foi adicionado. Em LCOM o processo de compilação é simples pois existe sempre um makefile que auxilia na tarefa. Para compilar basta correr os seguintes comandos:

minix$ make clean # apaga os binários temporários
minix$ make       # compila o programa

Testagem do código

A biblioteca LCF (LCOM Framework) disponível nesta versão do Minix3 tem um conjunto de testes para cada função a implementar em lab5.c. Assim é simples verificar se o programa corre como esperado para depois ser usado sem problemas no projeto. Para saber o conjunto dos testes disponíveis basta consultar:

minix$ lcom_run lab5

Neste caso em concreto estão disponíveis várias combinações:

minix$ lcom_run lab5 "init <105,110,115,11A,14C> <SECONDS> -t 1"
minix$ lcom_run lab5 "rectangle <105,110,115,11A,14C> <X> <Y> <DELTA_X> <DELTA_Y> <COLOR_HEX> -t 1"
minix$ lcom_run lab5 "pattern <105,110,115,11A,14C> <N_RECTANGLES> <COLOR_HEX> <STEP> -t 1"
minix$ lcom_run lab5 "xpm <XPM_INDEX> <X> <Y> -t 1"
minix$ lcom_run lab5 "move <XPM_INDEX> <XI>:<YI> <XF>:<YF> <SPEED>:<FPS> -t <0,1,2>"

@ Fábio Sá
@ Março de 2023