Tutorial de Assembler de Adam Hyde 1.0

PARTE 6
Traduzido por Renato Nunes Bastos


Versão   :  1.1
Data       :  13-04-1996 / online by Renato 03-11-1998
Contato  :  blackcat@vale.faroc.com.au
                 http://www.faroc.com.au/~blackcat
;Renato   :   rnbastos@ig.com.br 
                  http://www.geocities.com/SiliconValley/Park/3174


Introdução / Rotina do último tutorial | In e Out | Mais Flags


Olá de novo, programadores de Assembler. Esta edição demorou um pouco, mas eu tinha muita coisa para terminar, e eu estou trabalhando em um jogo meu agora. É um jogo de estratégia, como Warlords II, e acho que eu vou ter que escrever
a maior parte do código em 640x480, não meu amado 320x200 - mas eu posso mudar de idéia. Eca, a quantidade de jogos que eu comecei a escrever mas nunca cheguei a terminar é enorme, e este não deve chegar muito longe.

De qualquer modo, eu disse que daríamos uma olhada numas rotinas de linha e círculo esta semana, então, vamos lá...


Semana passada nós chegamos à seguinte rotina de linhas horizontais -

      mov   ax, 0A000h
      mov   es, ax         ; Aponta ES para a VGA

      mov   ax, x1         ; AX = X1
      mov   bx, y           ; BX = Y
      mov   cx, x2         ; CX = X2

      sub   cx, ax          ; CX = Diferença de X2 e X1

      mov   di, ax          ; DI = X1
      mov   dx, bx         ; DX = Y
      shl   bx, 8             ; Y SHL 8
      shl   dx, 6             ; Y SHL 6
      add   dx, bx         ; DX = Y SHL 8 + Y SHL 6
      add   di, dx          ; DI = Offset do primeiro pixel

      mov   al, color     ; Põe a cor a plotar em AL
      rep   stosb           ; Desenha a linha

Agora, embora essa rotina seja muito mais rápida que as rotinas do BGI, (ou seja lá o que for que seu compilador tenha), ela poderia ser melhorada pra caramba. Se entrarmos na rotina, com a lista de clocks que eu dei no último tutorial, você vai ver que ela gasta bem pouco.

Eu vou deixar a otimização com você por enquanto, (vamos ver isso em outro tutorial), mas se substituir STOSB por MOV ES:[DI], AL ou STOSW vai melhorar muito as coisas. Não se esqueça que se você decidir usar um loop, para jogar words na VGA, você terá que decrementar CX de uma unidade.

Agora, vamos ver uma linha vertical. Teremos que calcular o offset do primeiro pixel como nós fizemos na rotina de linhas horizontais, então, algo desse tipo funcionaria:

   mov   ax, 0A000h      ; Põe o segmento VGA em AX
   mov   es, ax                ; Aponta ES para a VGA

   mov   ax, Y1           ; Move o primeiro valor de Y para AX
   shl   ax, 6                ; Y x 26
   mov   di, ax             ; Move o novo valor de Y para DI
   shl   ax, 2                ; Agora temos Y = Y x 320
   add   di, ax              ; Adiciona aquele valor a DI
   add   di, X               ; Soma o valor de X a DI

Agora umas coisas básicas...

   mov   cx, Y2           ; Guarda Y2 em CX
   mov   al, Color       ; Guarda a cor a plotar em AL
   sub   cx, Y1            ; CX = tamanho da linha

E agora o loop final...

Plota:
   mov   es:[di], al     ; Põe um pixel no offset corrente
   add   di, 320         ; Move para a próxima linha
   dec   cx                 ; Decrementa CX de um
   jnz   Plota              ; Se CX <> 0, então continua plotando

Não é uma rotina fantástica, mas é muito boa. Note como foi possível realizar uma comparação depois de DEC CX. Isto é um conceito extremamente útil, logo, não se esqueça de que isso é possível.

Brinque um pouco com o código, e tente fazê-lo mais rápido. Tente outros métodos de calcular o offset, ou métodos diferentes de controle de fluxo.


Agora, isso foi a coisa fácil. Vamos ver agora uma rotina capaz de desenhar linhas diagonais.

A seguinte rotina foi tirada de SWAG, autor desconhecido, e é uma rotina ideal para demonstrar um algoritmo de linhas. Ele está precisando muito de uma otimização, assim, essa pode ser uma tarefa para você - se você quiser. Alguns dos pontos a considerar são:

   1) Seja lá quem o escreveu nunca ouviu falar de XCHG - isso economizaria
        alguns clocks;

   2) Ele comete um dos grandes pecados do código não-otimizado - ele move
        um valor para AX, e então realiza uma operação envolvendo AX na próxima
        instrução, assim fazendo um ciclo a mais. (Vamos falar sobre isso semana
        que vem).

   3) Ele trabalha com BYTES e não WORDS, assim, a velocidade de escrita para a
        VGA poderia se dobrada se usasse words.

   4) E o maior pecado de todos, ele usa um MUL para achar o offset. Tente
       usar shifts ou um XCHG para acelerar as coisas.

De qualquer modo, eu pus os comentários nele, e acho que ele é auto-explicativo, assim, eu não vou entrar em detalhes como ele funciona. Você deve ser capaz de pegar isso sozinho. Adentre a rotina, e veja como a derivada (gradiente, variação, inclinação...) da linha foi calculada.

Procedure Line(X1, Y1, X2, Y2 : Word; Color : Byte);   Assembler;

Var
   DeX          : Integer;
   DeY          : Integer;
   IncF         : Integer;

Asm     { Line }
   mov   ax, [X2]     { Move X2 para AX                                     }
   sub   ax, [X1]      { Pega o tamanho horizontal da linha    (X2 - X1)    }
   jnc   @Dont1      { X2 - X1 é negativo?                                 }
   neg   ax                { Sim, então faz com que seja positivo                }

@Dont1:
   mov   [DeX], ax    { Agora, move o tamanho horizontal da linha para DeX }
   mov   ax, [Y2]       { Move Y2 para AX                                     }
   sub   ax, [Y1]        { Subtrai Y1 de Y2, dando o tamanho vertical         }
   jnc   @Dont2        { Foi negativo?                                       }
   neg   ax                  { Sim, então faça-o positivo                          }

@Dont2:
   mov   [DeY], ax     { Move o tamanho vertica para DeY                     }
   cmp   ax, [DeX]     { Compara o tamanho vertivcal com o horizontal       }
   jbe   @OtherLine   { Se o vertical foi <= horizontal então pula         }

   mov   ax, [Y1]           { Move Y1 para AX                                     }
   cmp   ax, [Y2]           { Compara Y1 a Y2                                     }
   jbe   @DontSwap1   { Se Y1 <= Y2 então pula, senão...                    }
   mov   bx, [Y2]           { Põe Y2 em BX                                        }
   mov   [Y1], bx           { Põe Y2 em Y1                                        }
   mov   [Y2], ax           { Move Y1 para Y2                                     }
                                   { Para que depois de tudo isso.....                   }
                                   { Y1 = Y2 e Y2 = Y1                                   }

   mov   ax, [X1]      { Põe X1 em AX                                        }
   mov   bx, [X2]      { Põe X2 em BX                                        }
   mov   [X1], bx      { Põe X2 em X1                                        }
   mov   [X2], ax      { Põe X1 em X2                                        }

@DontSwap1:
   mov   [IncF], 1            { Põe 1 em IncF, i.e., plota outro pixel             }
   mov   ax, [X1]             { Põe X1 em AX                                        }
   cmp   ax, [X2]             { Compara X1 com X2                                   }
   jbe   @SkipNegate1  { Se X1 <= X2 então pula, senão...                    }
   neg   [IncF]                 { Nega IncF                                           }

@SkipNegate1:
   mov   ax, [Y1]      { Move Y1 para AX                                     }
   mov   bx, 320       { Move 320 para BX                                    }
   mul   bx                 { Multiplica 320 por Y1                               }
   mov   di, ax           { Põe o resultado em DI                               }
   add   di, [X1]       { Soma X1 a DI, e tcham - offset em  DI               }
   mov   bx, [DeY]   { Põe DeY em BX                                       }
   mov   cx, bx         { Põe DeY em CX                                       }
   mov   ax, 0A000h{ Põe o segmento a ser plotado, em AX                 }
   mov   es, ax         { ES aponta para a VGA                                }
   mov   dl, [Color]  { Põe a cor a usar em DL                              }
   mov   si, [DeX]    { Aponta SI para DeX                                  }

@DrawLoop1:
   mov   es:[di], dl   { Põe a cor a plotar, DL, em ES:DI                    }
   add   di, 320       { Soma 320 a DI, i.e., próxima linha abaixo          }
   sub   bx, si           { Subtrai DeX de BX, DeY                              }
   jnc   @GoOn1   { Ficou negativo?                                     }
   add   bx, [DeY]  { Sim, então soma DeY a BX                            }
   add   di, [IncF]   { Soma a quantidade a incrementar a DI                }

@GoOn1:
   loop  @DrawLoop1     { Nenhum resultado negativo, então plota outro pixel }
   jmp   @ExitLine            { Acabou, então vamos embora!                         }

@OtherLine:
   mov   ax, [X1]               { Move X1 para AX                                     }
   cmp   ax, [X2]               { Compara X1 a  X2                                    }
   jbe   @DontSwap2      { X1 <= X2 ?                                          }
   mov   bx, [X2]               { Não, então move X2 para BX                          }
   mov   [X1], bx               { Move X2 para X1                                     }
   mov   [X2], ax                { Move X1 para X2                                     }
   mov   ax, [Y1]                { Move Y1 para AX                                     }
   mov   bx, [Y2]                { Move Y2 para BX                                     }
   mov   [Y1], bx                { Move Y2 para Y1                                     }
   mov   [Y2], ax                { Move Y1 para Y2                                     }

@DontSwap2:
   mov   [IncF], 320       { Move 320 para IncF, i.e., o próximo pixel está na  }
                                     { próxima linha                                       }
   mov   ax, [Y1]             { Move Y1 para AX                                     }
   cmp   ax, [Y2]             { Compara Y1 a  Y2                                    }
   jbe   @SkipNegate2   { Y1 <= Y2 ?                                          }
   neg   [IncF]                  { Não, então nega IncF                                }

@SkipNegate2:
   mov   ax, [Y1]           { Move Y1 para AX                                     }
   mov   bx, 320            { Move 320 para BX                                    }
   mul   bx                     { Multiplica AX por 320                               }
   mov   di, ax               { Move o resultado para DI                            }
   add   di, [X1]            { Soma X1 a DI, dando o   offset                       }
   mov   bx, [DeX]       { Move DeX para BX                                    }
   mov   cx, bx              { Move BX para CX                                     }
   mov   ax, 0A000h    { Move o endereço da VGA para AX                      }
   mov   es, ax               { Aponta ES para a VGA                                }
   mov   dl, [Color]       { Move a cor a plotar para DL                         }
   mov   si, [DeY]         { Move DeY para SI                                    }

@DrawLoop2:
   mov   es:[di], dl   { Põe o byte em DL para ES:DI                         }
   inc   di                  { Incrementa DI de um, o próximo pixel                }
   sub   bx, si            { Subtrai SI de BX                                    }
   jnc   @GoOn2     { Ficou negativo?                                     }
   add   bx, [DeX]   { Sim, então soma DeX a BX                            }
   add   di, [IncF]     { Soma IncF a DI                                      }

@GoOn2:
   loop  @DrawLoop2    { Continua plotando                                   }

@ExitLine:
                       { Pronto!                                             }
End;

Acho que não fiz nenhum erro com os comentários, mas eu estou bem cansado, e não tenho bebido cafeína há dias - se você encontrar um erro - por favor me diga.

Eu ia colocar um algoritmo de círculo, mas eu não consegui fazer a minha funcionar em Assembler - toda aquela matemática de ponto flutuante deve ter algo a ver com isso. Eu poderia incluir uma escrita em linguagem de alto nível, mas eu suponho que esse seja um tutorial de Assembler, não de gráficos. Contudo, se pessoas suficientes reclamarem, querendo uma...


   OS INS E OUTS DE IN E OUT

IN e OUT são uma parte muito importante de código em Assembler. Elas permitem você a mandar/receber diretamente dados de qualquer uma das  65,536 portas de hardware, ou registradores. A sintaxe básica é como segue:

                                    Descrição: Esta instrução lê um valor
                                    de uma das 65536 portas de hardware
                                    para o acumulador especificado.

                                    AX e AL são comumente usados para portas
                                    de entrada, e DX é mais usado para
                                    identificar a porta.

                                    EX.: IN    AX, 72h
                                            MOV    DX, 3C7h
                                             IN     AL, DX

                                    Descrição: Esta instrução põe na saída o
                                    valor no acumulador para <PORTA>.  Usando
                                    o registrador DX para passar a porta para
                                    OUT, você pode acessar até 65,536 portas.

                                    EX.: MOV   DX, 378h
                                            OUT    DX, AX
 

OK, isso não ajudou muito, já que não disse muito sobre como usar - muito menos para que usar. Bem, se você pretende trabalhar muito com a VGA, você terá que ser capaz de programar seus registradores internos. Semelhantes aos registradores com que você tem trabalhado até agora, você pode pensar em mudá-los como interrupções, exceto que: 1) Você passa os valores para a porta, e é isso aí; e 2) É muito perto de ser instantâneo.

Como exemplo, vamos ver como setar e pegar a palette controlando diretamente o hardware da VGA.

Agora, a VGA tem uma porção de registradores, mas as próximas três é bom que você conheça bem:

O que tudo isso significa é -

Se nós fôssemos setar um valor RGB de uma cor RGB, nós mandaríamos o valor da cor que queríamos mudar para 03C8h, então ler os 3 valores de 03C9h. Em Assembler, faríamos isso:

   mov   dx, 03C8h        ; Põe o registrador DAC de leitura em DX
   mov   al, [Color]         ; Põe o valor da cor em AL
   out   dx, al                   ; Manda AL para a porta DX
   inc   dx                        ; Agora usa a porta 03C9h
   mov   al, [R]                ; Põe o novo valor VERMELHO em AL
   out   dx, al                   ; Manda AL para a porta DX
   mov   al, [G]                ; Põe o novo valor VERDE em AL
   out   dx, al                   ; Manda AL para a porta DX
   mov   al, [B]                ; Põe o novo valor AZUL em AL
   out   dx, al                   ; Manda AL para a porta DX

E aquilo deveria fazer a coisas direitinho. Para ler a palette, faríamos isso:

   mov   dx, 03C7h        ; Põe o registrador DAC de escrita em DX
   mov   al, [Color]         ; Põe o valor da cor em AL
   out   dx, al                   ; Manda AL para a porta DX
   add   dx, 2                   ; Agora usa a porta 03C9h

   in    al, dx                    ; Põe o valor conseguido da porta DX em AL
   les   di, [R]                  ; Aponta DI para a variável R - Isso vem do Pascal
   stosb                           ; Guarda AL em R

   in    al, dx             ; Põe o valor conseguido da porta DX em AL
   les   di, [G]           ; Aponta DI para a variável G
   stosb                    ; Guarda AL em G

   in    al, dx             ; Põe o valor conseguido da porta DX em AL
   les   di, [B]           ; Aponta DI para a variável B
   stosb                    ; Guarda AL em B

Note como essa rotina foi codificada diferentemente. Isso era originalmente uma rotina em Pascal, e como o Pascal não gosta que você mexa com variáveis de Pascal em Assembler, você tem que improvisar.

Se você está trabalhando com Assembler puro, então você pode codificar isso muito mais eficientemente, como o primeiro exemplo. Eu deixei o código como estava para que aqueles trabalhando com uma linguagem de alto nível possam chegar a um problema particularmente irritante.

Agora você já viu como IN e OUT podem ser úteis. Controlar diretamente o hardware é mais rápido e mais eficiente. Nas próximas semanas, eu posso incluir uma lista das portas mais comuns, mas se você tivesse uma cópia da Ralf Brown's Interrupt List (disponível no X2FTP), você já teria uma cópia.

OBS.: Você pode achar um link para a Ralf Brown's Interrupt List na minha página.


Um pouco mais sobre o registrador de flags:

Agora, embora tenhamos usado o registrador de flags em quase todo nosso código até esse ponto, eu não entrei profundamente nesse assunto. Você pode trabalhar felizmente sem conhecer muito sobre os flags, e comparar coisas sem saber o que está realmente acontecendo, mas se você quiser avançar no Assembler, você precisa saber algo mais.

De volta ao Tutorial Três, eu dei uma visão simplista do registrador de FLAGS. Na realidade, os FLAGS, ou EFLAGS é na verdade um registrador de 32-bit, embora apenas só os bits de 0 a 18 sejam usados. Na realidade não precisamos conhecer os flags acima do 11 por enquanto, mas é bom saber que eles existem.

O registrador EFLAGS na verdade se parece com isso:

18  17  16  15  14  13  12  11  10  09   08  07  06  05  04  03  02  01  00
AC  VM  RF  --  NT  IO/PL   OF  DF  IF   TF  SF  ZF  --  AF  --  PF  --  CF

Agora, os flags são os seguintes:

Agora, de todos esses acima, você não precisa mesmo se preocupar muito com a maioria deles. Por enquanto, só conhecer CF, PF, ZF, IF, DF e OF será suficiente. Eu não dei comentários para os primeiros já que eles são puramente técnicos, e são usados mais no modo protegido e situações complexas. Você não deveria ter que conhecê-los.

Você pode, se quiser, mover uma copia do flags para AH com LAHF - (Carrega AH com Flags) - e modificar ou ler bits individualmente, ou mudar o status dos bits mais facilmente com CLx e STx. Contudo se você planeja mudar os flags, lembre-se de que eles podem ser extremamente úteis em muitas situações.

(Eles podem também ser muito enjoados quando tarde da noite, linhas começam a desenhar para trás, e você gasta um hora procurando o porquê - e então se lembra que você se esqueceu de limpar o flag de direção!)


Acho que cobrimos muito pouca coisa importante neste tutorial. Dê uma olhada nos flags, e volte naquela rotina compridona de fazer linhas, já que ela é um ótimo exemplo de controle de fluxo. Assegure-se de que suas capacidades de controlar fluxos de intruções estão perfeitas.

Semana que vem, vou tentar amarrar todos os tópicos que vimos estas poucas semanas juntos, e apresentar alguma forma de revisão de tudo que você aprendeu. Semana que vem eu também vou entrar em otimização, e como você pode acelerar todo o código com que temos trabalhado até agora.


No próximo tutorial vamos ver:

Se você deseja ver um tópico discutido num tutorial no futuro, escreva-me, e eu vou ver o que eu posso fazer.


Não perca!!! Baixe o tutorial da próxima semana na minha homepage:

Vejo vocês na próxima semana!

- Adam.
- Renato Nunes Bastos