Este template reune aspetos relevantes para o projeto final de LCOM, como dicas, algoritmos, orientação na estruturação do código, otimizações possíveis entre outros.
- Setup
- Funcionalidades implementadas
- Estrutura MVC
- Orientação a Objetos em C
- Máquinas de Estado em C
- XPM em modo direto
- Otimizações
- Debug
O template pode ser executado com as seguintes instruções:
$ git clone https://github.com/Fabio-A-Sa/Y2S2-LabComputadores.git
$ cd Y2S2-LabComputadores/Template
$ make clean && make
$ lcom_run proj
O programa depende das configurações colocadas em config.h
. É possível mudar a frequência, tamanhos de ecrã, modos de cores e verificar a importância da otimização double buffering.
#define GAME_FREQUENCY 60 // 30
#define VIDEO_MODE 0x115 // 0x14C
#define DOUBLE_BUFFER 1 // 0
Com a variação destes parâmetros conclui-se que:
- Uma frequência superior a 60 Hz não apresenta ganhos significativos;
- Modos superiores a 0x115 podem ser lentos (maior memória manipulada, mais bytes por pixel, maior resolução);
- A aplicação implementada com double buffering é substancialmente mais fluída;
Apesar do programa não fazer nada de útil, apresenta praticamente todos os truques necessários para realizar o projeto: gestos do cursor, cliques, lidar com diferentes objetos, diferentes menus e interação entre os diversos dispositivos.
O template tem três menus:
- A tecla
S
acede ao menu start; - A tecla
G
acede ao menu game; - A tecla
E
acede ao menu end;
Em qualquer momento, a tecla Q
permite terminar a execução. Em todos os menus o cursor está presente e funcional.
Menu G
No menu G
é possível clicar num de quatro botões, alterando a cor do quadrante respetivo. O cursor nesta etapa tem outro formato além do tradicional.
Baseado nos slides do André Restivo.
MVC
O modelo conhece os objetos do programa, como Botões, o Cursor, Sprites, o estado do Menu, o estado do Sistema. As interrupções tratadas no controlador afetam diretamente o modelo e as interações entre os objetos através de máquinas de estado.
Contém todos os controladores dos dispositivos implementados durante as aulas práticas. Todas as funções de alto nível, nomeadamente as presentes em ficheiros labX.c, foram eliminadas. De igual forma partes do código foram modificadas para uma melhor modularidade. Por exemplo:
- Agora a subscrição das interrupções é realizadas sem passagem da máscara como argumento;
- A estrura mouse_info_t foi substituida por MouseInfo, que é mais compacta e adaptada às necessidades da aplicação;
- VBE.h agora também contém as cores manipuladas pela VRAM;
Módulo que trata da representação visual do modelo sempre que há uma interrupção por parte do timer.
O objeto Sprite, usado para criar e manipular os elementos do cenário, é na realidade uma struct:
typedef struct {
uint16_t height; // altura, em pixeis
uint16_t width; // largura, em pixeis
uint32_t *colors; // array de cores, se for criado a partir de um XPM
uint32_t color; // cor única, se for um rectângulo simples como o botão
uint8_t pressed; // indica se o objeto foi pressionado pelo rato
} Sprite;
Por motivos de eficiência nem sempre são criados com XPM. Por exemplo, no caso do botões que têm cor única, é aconselhável usar apenas o parâmetro color e não preencher o array colors:
#include "xpm/mouse.xpm"
Sprite *mouse;
Sprite *button1;
void setup_sprites() {
mouse = create_sprite_xpm((xpm_map_t) mouse_xpm);
button1 = create_sprite_button(mode_info.XResolution/2, mode_info.YResolution/2, ORANGE);
}
Sprite *create_sprite_xpm(xpm_map_t sprite);
Sprite *create_sprite_button(uint16_t width, uint16_t height, uint32_t color);
Ao lidar com apontadores e alocação dinâmica de memória é possível tratar a estrutura como se fosse um objeto em C++:
void update_buttons_state() {
if (mouse_info.left_click) {
if (mouse_info.x < mode_info.XResolution/2 && mouse_info.y < mode_info.YResolution/2)
button1->pressed = 1;
//...
}
}
É boa prática antes de acabar o programa libertar a memória alocada:
void destroy_sprite(Sprite *sprite) {
if (sprite == NULL) return;
if (sprite->colors) free(sprite->colors);
free(sprite);
sprite = NULL;
}
A gestão das interrupções geradas pelos dispositivos estudados até aqui pode constituir um modo de Event Driven Design
. Nesse caso o fluxo do programa é controlado pelo ambiente onde está inserido, ou seja, é reativo na resposta aos eventos (interrupções) que poderão ocorrer de forma assíncrona. No entanto, para o contexto do Projeto de LCOM este design de código não é suficiente para garantirmos um código robusto, modular e facilmente manipulável. Aqui há um exemplo mais complexo do que o apresentado de seguida.
A transição entre menus é semelhante a transições numa máquina de estados:
Máquina de Estados para Menus
Em C um conjunto de estados pode ser programado usando uma enumeração e as transições com recurso a switch-case:
typedef enum {
RUNNING,
EXIT,
} SystemState;
typedef enum {
START,
GAME,
END
} MenuState;
// condições iniciais
SystemState systemState = RUNNING;
MenuState menuState = START;
// a máquina de estados é atualizada de acordo com
// o valor do scancode lido em cada interrupção
void update_keyboard_state() {
(kbc_ih)();
switch (scancode) {
case Q_KEY:
systemState = EXIT;
break;
case S_KEY:
menuState = START;
break;
case G_KEY:
menuState = GAME;
break;
case E_KEY:
menuState = END;
default:
break;
}
}
A atualização do estado tem consequências imediatas. Do lado do módulo View, o estado é consultado e a partir daí é escolhido o menu a desenhar no novo frame:
extern MenuState menuState;
void draw_new_frame() {
switch (menuState) {
case START:
draw_initial_menu();
break;
case GAME:
draw_game_menu();
break;
case END:
draw_finish_menu();
break;
}
draw_mouse();
}
A ordem da pintura é importante. Segundo o Algoritmo do Pintor, por questões de visibilidade pinta-se primeiro os píxeis mais afastados (o fundo da tela, o menu), sobrepondo os píxeis de objetos mais próximos (o cursor, os botões).
X PixMap (XPM) é uma forma de representação de imagens. O ficheiro fornecido no lab5 possuia alguns exemplos de XPMs com cores no formato indexado. Para o projeto convém utilizar XPMs em modo direto e em princípio é possível gerar qualquer imagem XPM a partir de uma imagem JPG ou PNG. Sugere-se usar imagens com cor de fundo constante e não muito grandes.
O software gratuito que permite gerar XPMs de forma mais simples é o GIMP. Ao abrir uma imagem qualquer:
Abrir uma imagem
Em "File > Export As.." dá para renomear a imagem de output e escolher o formato XPM:
Exportar para formato XPM
O ficheiro gerado é este:
static char * smile_xpm[] = {
"200 200 621 2",
" c #FFFFFE", // Cor transparente
". c #000000",
"+ c #060606",
//...
A forma de lidar com XPMs de cor direta é diferente da indicada nos Labs. Primeiro temos de garantir que a cor de fundo é uma cor conhecida para poder ser ignorada na altura de alterar os pixeis do frame buffer. Por exemplo, no caso do template foi escolhida a cor 0xFFFFFE, que só difere 1 bit do valor máximo permitido em RGB, logo será pouco provável que apareça como uma cor relevante na imagem. Essa será a cor transparente.
#define TRANSPARENTE 0xFFFFFE
#include "xpm/smile.xpm"
Sprite *smile;
void setup_sprites() {
smile = create_sprite_xpm((xpm_map_t) smile_xpm);
}
A função create_sprite_xpm
constroi um array de cores de acordo com a estrutura do XMP dado como parâmetro:
Sprite *create_sprite_xpm(xpm_map_t sprite){
Sprite *sp = (Sprite *) malloc (sizeof(Sprite));
if( sp == NULL ) return NULL;
xpm_image_t img;
sp->colors = (uint32_t *) xpm_load(sprite, XPM_8_8_8_8, &img);
sp->height = img.height;
sp->width = img.width;
if( sp->colors == NULL ) {
free(sp);
return NULL;
}
return sp;
}
A imagem é processada pixel a pixel, com consulta da cor dos pixeis no array do sprite. Se a cor consultada for igual à transparente não ocorre nenhuma cópia. Assim há garantias de transparência desta parte do frame:
int draw_sprite_xpm(Sprite *sprite, int x, int y) {
uint16_t height = sprite->height;
uint16_t width = sprite->width;
uint32_t current_color;
for (int h = 0 ; h < height ; h++) {
for (int w = 0 ; w < width ; w++) {
current_color = sprite->colors[w + h*width];
if (current_color == TRANSPARENTE) continue;
if (draw_pixel(x + w, y + h, current_color, drawing_frame_buffer) != 0) return 1;
}
}
return 0;
}
As flags de compilação -O2
e -D_LCOM_OPTIMIZED_
ajudam o compilador a otimizar o código para o Minix. É importante utilizá-las para garantir uma execução mais fluída. No makefile:
CFLAGS += -pedantic -D_LCOM_OPTIMIZED_ -O2
É a principal e mais importante otimização a usar no projeto. Em config.h
, ao desativar a flag DOUBLE_BUFFER
é possível notar que o ecrã do template fica muito mais travado. O fenómeno é chamado de Flicker Screen e ocorre porque o mesmo frame buffer é usado, simultaneamente, para:
- suportar qualquer mudança de posição ou cor dos objetos do ecrã;
- imprimir o ecrã píxel a píxel;
Assim, mesmo com uma frequência baixa é muito provável que a consulta de um pixel seja acompanhada por uma atualização do frame todo, causando um delay não desejado.
A solução clássica usada na Computação Gráfica é termos com dois buffers:
- O primeiro buffer, o buffer principal que é o único que tem tradução física na VRAM, apenas é atualizado quando ocorre uma interrupção do timer. A atualização é uma cópia integral do conteúdo do segundo buffer para este;
- O segundo buffer, o buffer secundário, é atualizado sempre que haja alguma mudança de estado dos objetos que compõem o ecrã;
Double Buffering
uint8_t *main_frame_buffer;
uint8_t *secondary_frame_buffer;
uint32_t frame_buffer_size;
// Tratamento da interrupção do timer
void update_timer_state() {
swap_buffers();
}
// Tratamento da interrupção do Mouse
void update_mouse_state() {
(mouse_ih)();
mouse_sync_bytes();
if (byte_index == 3) {
mouse_sync_info();
update_buttons_state();
draw_new_frame(); // Desenha a atualização no frame buffer secundário
byte_index = 0;
}
}
// Cópia de todo o conteúdo do buffer secundário para o primário
void swap_buffers() {
memcpy(main_frame_buffer, secondary_frame_buffer, frame_buffer_size);
}
- O fenómeno Flicker Screen deixa de ser visível;
- Solução simples de implementar;
- Gasta o dobro da memória;
- A cópia da memória do buffer secundário para o principal tem complexidade temporal O(N), linear com o tamanho do buffer;
- Há um delay entre uma interrupção de um dispositivo e a mudança visual do estado afetado por essa interrupção. Esse delay é de no máximo (1/frequência) segundos;
Devemos evitar fazer a mesma operação binária várias vezes. Por exemplo, uma implementação naive da função swap_buffers()
anterior poderia ser a seguinte:
void swap_buffers() {
memcpy(main_frame_buffer, secondary_frame_buffer,
mode_info.XResolution * mode_info.YResolution * ((mode_info.BitsPerPixel + 7) / 8));
}
Por cada interrupção do Timer ocorrem duas multiplicações, uma soma e uma divisão. Se considerarmos um funcionamento normal de 60 Hz, 60 interrupções por segundo portanto, ocorrem 60*(2+1+1)=240 operações por segundo só neste passo. Uma possível solução para ser mais eficiente:
uint32_t frame_buffer_size;
int set_frame_buffers(uint16_t mode) {
//...
frame_buffer_size = mode_info.XResolution * mode_info.YResolution * ((mode_info.BitsPerPixel + 7) / 8);
//...
}
void swap_buffers() {
memcpy(main_frame_buffer, secondary_frame_buffer, frame_buffer_size);
}
Com este pequeno truque poupamos milhares e milhares de operações aritméticas ao processador durante a execução do projeto. O Minix agracede.
A LCF (LCOM Framework) proporciona uma ferramenta de debug que escreve em dois ficheiros:
output.txt
, contém todos os outputs da última execução do projeto;trace.txt
, contém a ordem das chamadas das funções, os tipos e valores de retorno de cada uma delas;
O path do ficheiro a ser escrito na função main depende da configuração de cada máquina virtual. Para descobrir a string correcta, dentro da pasta desejada na consola do Minix executa-se o comando:
$ pwd # /home/lcom/labs/Template/debug
No meu caso direcionei os ficheiros escritos para o diretório /debug
para ficarem mais organizados.
int (main)(int argc, char *argv[]) {
lcf_set_language("EN-US");
lcf_trace_calls("/home/lcom/labs/Template/debug/trace.txt");
lcf_log_output("/home/lcom/labs/Template/debug/output.txt");
if (lcf_start(argc, argv)) return 1;
lcf_cleanup();
return 0;
}
@ Fábio Sá
@ Abril de 2023