Exemplo simples do uso de GtkBuilder

Ok… existe um jeito de construir uma janela e atribuir os sinais aos widgets sem escrever muito código usando o Glade. O exemplo abaixo é bem simples: Apenas uma janelinha com um label e um button:

Essa já é a janela da aplicação rodando!

Para fazer isso vamos ao Glade criar uma GtkWindow e, dentro dela, colocar uma GtkBox com orientação vertical (o padrão) e duas divisões:

Note que mudei o nome do objeto da janela para window, ao invés do default window1. Modifiquei também a propriedade title na aba “General” para “Hello”. O motivo de colocarmos a GtkBox acima é que queremos colocar uma janela filha embaixo da outra. Daí, colocamos elas na GtkBox e temos algo assim:

Depois de modificadas as características visuais de cada objeto vou às abas “Signals” tanto da window quanto de button1 e colocarei o nome da função gtk_main_quit nos eventos destroy e clicked:

Tudo o que temos que fazer agora é salvar o projeto no formato GtkBuilder e fazer um programinha para carregá-lo:

#include <gtk/gtk.h>

int main(int argc, char *argv[])
{
  GtkBuilder *builder;
  GtkWidget *window;

  gtk_init(&argc, &argv);

  builder = gtk_builder_new();

  // "App.glade" é o arquivo que salvamos antes!
  // Existem outros lugares de onde podemos obter o XML!
  gtk_builder_add_from_file(builder, "App.glade", NULL);

  // Conecta os eventos assinalados no XML com as 
  // respectivas funções.
  gtk_builder_connect_signals(builder, NULL);

  // Pega o objeto window e mostra-o.
  window = GTK_WIDGET(gtk_builder_get_object(builder,
                      "window"));
  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

Voilà! Basta compilar e executar:

$ gcc -O3 -s -o hello hello.c \
`pkg-config --cflags --libs gtk+-3.0`

Repare que isso ai exige o GTK+3.x.

UPDATE:

Duas pequenas modificações sem muita importância… É conveniente que o evento “clicked” do botão requisite que a janela seja fechada. Isso pode ser feito modificando o evento para chamar gtk_window_close com o parâmetro window (aparecerá uma lista com os objetos contidos na janela ao clicar em “User Data”).

A outra modificação é quanto às própria janela window, o label label1 e o botão button1. Em window, habilite os tamanhos default e ajuste-os em 320×200 (por exemplo). No label1, na aba “Packing”, habilite “Expand”. E em button1, mude o “Button Content” para “Stock Button” e selecione “gtk-close” na combobox. A diferença na janela final será essa:

No caso do botão, se seu sistema estiver em português, aparecerá “Fechar” e se os botões com ícones estiverem habilitados, aparecerá um x vermelho, ao lado… :)

Programação Gráfica em C: Gnome Tool Kit (GTK+)

Se você gostou da pequena série de textos sobre programação gráfica usando a Win32 API (aqui, aqui e aqui), saiba que apenas arranhei o assunto, mas toda a base está lá… Até mesmo para ambientes diferentes como X11, no Linux e Unixes em geral. É claro que a coisa toda é um pouco mais simples e diferente nesses outros ambientes.

No caso do Linux (e Unixes) o ambiente gráfico não é integrado ao sistema operacional. Trata-se de um software “servidor” ou daemon. A aplicação literalmente envia comandos para o daemon pedindo para desenhar uma janela, movê-la, mudar de tamanho, pintar uma área e o daemon manda mensagens de volta… Em essência, é o que o Windows também faz. Mas, no X11, as janelas são desenhadas por um desktop environment manager, que é um módulo que roda sobre o daemon X11. Existem vários: Gnome, Unity, Wayland, Cinnamon, MATE, KDE, XFCE, WindowMaker, Motif etc.

Os dois desktop managers mais famosos e mais aceitos são Gnome e KDE e, por isso, as duas bibliotecas mais usadas para desenvolvimento “em janelas” para Linux são o GTK+ e o Qt. O primeiro é uma conjunto de bibliotecas para serem usadas em C. A outra, é uma class library, um conjunto de bibliotecas para serem usadas em C++. Neste aspecto, o GTK+ é mais parecido com a Win32 API do que o Qt. Eis um “hello, world” com o GTK+:

#include <stdlib.h>
#include <gtk/gtk.h>

int main(int argc, char *argv[])
{
  GtkWidget *mainWindow;
  GtkWidget *label;

  gtk_init(&argc, &argv);

  // Cria a janela, ajusta o tamanho, o título e
  // conecta o evento "destroy" à função gtk_main_quit.
  mainWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  gtk_window_set_default_size(GTK_WINDOW(mainWindow),
                              320, 200);
  gtk_window_set_title(GTK_WINDOW(mainWindow), 
                       "Hello App");
  gtk_signal_connect(GTK_OBJECT(mainWindow),
                     "destroy",
                     GTK_SIGNAL_FUNC(gtk_main_quit),
                     NULL);

  // Cria o label e ajusta o texto.
  label = gtk_label_new(NULL);
  gtk_label_set_text(GTK_LABEL(label), "Hello, world!");

  // Adiciona o label à janela.
  // Mostra a janela e seus filhos.
  gtk_container_add(GTK_CONTAINER(mainWindow), label);
  gtk_widget_show_all(mainWindow);

  // Loop principal.
  gtk_main();

  return EXIT_SUCCESS;
}

Para compilar é sempre bom usar um utilitário que nos dará a lista de diretórios onde acharmos os headers e as libraries:

$ pkg-config --cflags gtk+-2.0
-pthread -I/usr/include/gtk-2.0 -I/usr/lib/x86_64-linux-gnu/gtk-2.0/include \
-I/usr/include/gio-unix-2.0/ -I/usr/include/cairo -I/usr/include/pango-1.0 \
-I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pixman-1 \
-I/usr/include/libpng12 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/libpng12 \
-I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/pango-1.0 \
-I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include \
-I/usr/include/freetype2

$ pkg-config --libs gtk+-2.0
-lgtk-x11-2.0 -lgdk-x11-2.0 -lpangocairo-1.0 -latk-1.0 -lcairo \
-lgdk_pixbuf-2.0 -lgio-2.0 -lpangoft2-1.0 -lpango-1.0 -lgobject-2.0 -lglib-2.0 \
-lfontconfig -lfreetype

Impressionante, não? GTK+ exige um porrilhão (como sempre, essa é uma especificação de grandeza! hehe) de diretórios contendo arquivos header e um punhado de bibliotecas! Para compilarmos o nosso programinha podemos misturar as opções --cflags e --libs em uma linha só, assim:

$ gcc -O3 -s -o hello hello.c `pkg-config --cflags --libs gtk+-2.0`

Estou usando as opções -O3 e -s para otimizações e eliminar símbolos não necessários no arquivo binário final. O resultado da execução de hello é esse:

Agora, mova a janela, mude o tamanho, minimize, maximize… tá tudo lá num pequeno executável de 10 KiB!

O que pode surpreender é o aparente uso de Orientação a Objetos em um código em C. Os macros GTK_WIDGET, GTK_WINDOW e GTK_LABEL são apenas type castings para que a função não reclame que estamos passando o tipo errado e, ao mesmo tempo, todo objeto visível é um widget (GtkWidget).

Para tornar as coisas mais rápidas, a identificação de cada objeto é, de fato, um ponteiro que aponta para ele próprio. Nada de “handles”, como no Win32. E, embora pudéssemos criar nosso próprio loop de mensagens, GTK+ fornece a função gtk_main() que faz isso por nós. Ele permite registrar callbacks para hooks no loop, como exemplo, quando o loop estiver esperando por mensagens, pode executar uma função idle

Ahhh… quanto ao pkg-config, use a opção --list-all para ver todas as bibliotecas que ele consegue obter as listas de headers e libs

Por que gtk_init() toma ponteiros para argc e argv?

A função gtk_init() pode receber opções da linha de comando diretamente para ele… No caso do X11, por exemplo, a opção --display indica o daemon onde as janelas serão criadas (pode estar em outra máquina, por exemplo!)… A função retira da lista de parâmetros aqueles que são destinados apenas ao GTK+!

Um aviso sobre o sistema de coordenadas usado no GTK+:

Se você já mexeu com Applets em JAVA, deve se lembrar da antiga biblioteca AWT (Abstract Widget Toolkit). Ele foi “copiado” de bibliotecas como XLib e GTK+… Os containers do GTK+ (GtkWindow, por exemplo) usam o sistema de coordenadas “geográfico” por default, onde, ao inserir um único widget-filho ele é colocado no “centro”, ocupando a área útil toda… mas, podemos colocá-lo ao norte, ao sul, ao leste ou oeste, assim como era feito no AWT.

Isso não significa que não podemos usar um sistema de coordenadas cartesiano, mas, para isso, precisaríamos incluir o container GtkFixed na nossa janela e esse container é o que permite posicionamento cartesiano… A maioria das aplicações feitas com GTK+ não fazem isso…

Existe um utilitário para desenhar janelas?

Sim… existe o Glade:

Screenshot do Glade

Ele gera um arquivo XML contendo a descrição de sua janela e o GTK+ possui funções que interpretam esse XML e montam a janela completa (GtkBuilder). É a perfeita implementação do MVC. O View é apenas o arquivo XML contendo a parte visual e o código que lida com ele é o seu Model e Controller. Num outro artigo explico como usá-lo…

Gtk+ é portável!

Um detalhe final é que GTK+ é portável. Existe o GTK+ para Linux, OS/X, FreeBSD, mas também tem para Windows! Isso quer dizer que você pode criar programas gráficos para todas as plataformas com o mesmo conjunto de bibliotecas. No caso do Windows, terá que usar o MinGW para compilar com o GCC e carregar junto um caminhão de DLLs que colocará em C:\Windows\System32 (aqui é mostrado como)… Note que é perfeitamente possível compilar sua aplicação com GTK+ usando o Visual Studio.

Note que, no Windows, não é necessário usar aquela construção biruta de WinMain(). O bom e velho main funciona!

GTK+ 2.0 e GTK+ 3.0

Existem duas implementações da biblioteca porque existem, hoje, dois “Gnomes” (versão 2.x e 3.x) diferentes… O ‘G’ do GTK vem de Gnome e é para desenvolver aplicações para ele que a biblioteca existe. Clique nos links para obter documentação de referência a ambas as versões: GTK+ 2.0 e GTK+ 3.0. Note, também, que a GLib é parte integrante e, portanto, uma pancada de algoritmos está à sua disposição (listas, árvores etc)…

Monitoramento com câmeras de vigilância do homem probre

Um amigo me perguntou se “alguém conhece algum software…” que crie uma grade com várias fontes de vídeo ao estilo de sistemas de monitoramento de segurança com câmeras… well… EU conheço: ffmpeg!

O ffmpeg é bem versátil: Ele permite a leitura e escrita em diversos meios, não apenas arquivos. Podemos usar protocolos como async, bluray, cache, concat, crypto, data, file, ftp, gopher, hls, http, httpproxy, https, mmsh, mmst, pipe, rtp, sctp, srtp, subfile, tcp, tls, udp, udplite, unix, rtmp, rtmpe, rtmps, rtmpt, rtmpte, sftp.

Suponha que você tenha 4 câmeras Go Pro, disponibilizando streams através de UDP. Em seu /etc/hosts elas serão conhecidas sob os nomes camera1.local, camera2.local, camera3.local e camera4.local e você quer criar um stream de vídeo com os 4 streams das câmeras numa grade 2×2… Os streams das câmeras serão obtidos via URL udp://camera1.local/stream, por exemplo e o stream de saída será enviado para o ffserver via URL http://localhost:8090/stream.ffm.

Esses são os nomes dos “arquivos” de entrada e saída que usaremos no ffmpeg. Mas, para facilitar a minha vida neste texto, para que eu não tenha que explicar como configurar o ffserver (leia a man page!), usarei “arquivos” fictícios de entrada nomeados 1.mp4, 2.mp4, 3.mp4 e 4.mp4 e o arquivo de saída será output.mp4, codificado com o codec h264 e sem áudio.

Para criarmos a grade podemos usar um filtro complexo (múltiplas entradas e uma saída), usando os filtros scale, hstack e vstack. O grafo do filtro fica assim:

Grafo para o filtro complexo

Os filtros hstack “empilham” dois ou mais entradas horizontalmente, enquanto vstack faz o mesmo verticalmente. Do jeito que o filtro está montado as fontes 1 e 2 ficarão na primeira linha e as 3 e 4 na segunda. Os filtros scale estão ai para garantir que todos os vídeos aplicados aos filtros de empilhamento terão o mesmo tamanho (por exemplo, 320×200). Assim, a linha de comando com o filtro complexo fica assim:

$ ffmpeg -i 1.mp4 -i 2.mp4 -i 3.mp4 -i 4.mp4 -an -c:v libx264 \
-filter_complex '[v:0]scale=320:200[t1];\
[v:1]scale=320:200[t2];\
[v:2]scale=320:200[t3];\
[v:3]scale=320:200[t4];\
[t1][t2]hstack[v1];\
[t3][t4]hstack[v2];\
[v1][v2]vstack[vout]' -map '[vout]' \
output.mp4

Se os vídeos tiverem duração diferentes, use a opção -shortest para usar a duração do menor deles, por exemplo… Isso não é problema se estivermos lidanco com streams.

E se eu tiver um número ímpar de fontes de vídeo?

Existe uma fonte “especial” chamada nullsrc, onde você pode especificar, num grafo, "nullsrc=s=320x200[vnull]" e a fonte vnull será um vídeo de 320×200 vazio.

Mas… ficou muito chapado!

Os filtros vstack e hstack colocam os vídeos lado a lado, sem espaçamento… Isso não significa que, ao invés de usar esses filtros, você não possa usar o filtro overlay para os vídeos, como mostrei neste post aqui, só que para imagens.

Você pode usar uma imagem PNG como vídeo de fundo, por exemplo, com “painéis”, criados com o GIMP onde os streams serão colocados. Basta posicionar os streams, depois de “escalonados” nos locais corretos.

Mas… Eu quero fazer um programinha!

Você terá que lidar com as bibliotecas da família libav, como mostrei neste post. Mas, atenção, o código do artigo não funciona mais… existem muitas diferenças entre as versões atuais das bibliotecas da família libav do que as da época em que escrevi aquele artigo! Consulte a documentação da versão de suas libs.

Além do mais… o código final ficará muito grande e complicado (teremos que obter frames de fontes diferentes, via rede e “servir” o stream final, trabalhado… Os escalonamentos e posicionamentos podem ainda ser feitos por filtros e, suspeito, que as bibliotecas sejam agnósticas quanto às fontes dos streams… mas, isso será mesmo uma complicação. Deixo para o leitor os detalhes de implementação sabendo que será uma pequena dor-de-cabeça.

O ffserver:

O usuário vai querer ver o stream final, ao invés dos individuais. O meio mais “simples” de fazer isso é usar o ffserver, que receberá o stream gerado acima e permitirá que um usuário conecte ao servidor usando seu player favorito (VLC, por exemplo).

O ffserver precisa de um arquivo de configuração informando os dados da conexão que ele esperará receber, bem como o formato do stream de saída. Ao invés de enviar a saída do ffmpeg para um arquivo output.mp4, enviamos para, por exemplo, http://localhost:8090/stream.ffm. Onde a porta 8090 é a usada (por default) pelo ffserver e stream.ffm o path definido no arquivo de configuração. Do lado do cliente, podemos ver o stream via URL (por exemplo): rtsp://<meuhost>/stream. Claro, depende de como você configura o ffserver.

Respostas aos meus leitores: A má compreendida função printf

Este post é um exemplo de reposta longa que darei no meu mais recente grupo no Facebook (C & ASM FAQ). Vou fazer de conta que alguém tenha me perguntado sobre o significado das “marcas de conversão” usadas na função printf (o “f” final vem de “formated”)… Essas marcas são aquelas sub strings começadas com “%” que seguem o padrão:

%[flag][width][.precision][modifier]specifier

Os termos entre colchetes são opcionais.

Quando printf encontra uma sub string começando com “%” ele verifica seu formato. Se um outro “%” seguir o primeiro, então o caracter ‘%’ é impresso, caso contrário, estamos dizendo à função para pegar o próximo argumento da função e convertê-lo para uma string, de acordo com o critério da marca.

Por exemplo:

printf("Valor: %d\n", x);

A marca “%d” diz ao printf que o próximo argumento (x) deve ser convertido para uma string numérica inteira e decimal. Se usássemos “%x”, ao invés de “%d”, o valor do próximo argumento seria convertido para uma string numérica inteira e hexadecimal, com os “algarismos” de ‘a’ até ‘f’ sendo mostrados em forma minúscula… Se substituirmos a marca por “%X” esses “algarismos” seriam mostrados de forma maiúscula.

As marcas de conversão padronizadas são “%c”, “%d” (ou “%i”), “%u”, “%x” (ou “%X”), “%o”, %e”, “%f”, “%g”, “%n” e “%s” para, respectivamente, “caracter”, “decimal inteiro (com sinal)”, “decimal inteiro (sem sinal)”, “hexadecimal inteiro”, “octal inteiro”, “ponto flutuante em ‘notação científica'”, “ponto flutuante ‘fracionário'”, “ponto flutuante (um ou outro anterior)”, “número de caracteres escritos” e “strings”. Dessas marcas, apenas o “%n” não exige um argumento adicional.

Modificador:

As marcas esperam tipos específicos nos parâmetros aos quais estão associadas. “%c” espera um tipo char. “%s” espera um ponteiro para char. “%d” e “%i” esperam um tipo int e as marcas que lidam com ponto flutuante esperam o tipo double. Assim, se quisermos imprimir um caracter do tipo wchar_t, temos que usar o modificador “l” (de “long”), como em “%lc”. A mesma coisa aplica-se ao modificador “%s”, se quisermos usar um ponteiro para wchar_t.

O mesmo modificador “l” é usado para “%d”, “%i” e “%u” para o uso do tipo long int. Mas, podemos querer usar um tipo char como container para um inteiro e, neste caso, podemos uasr o modificador “hh”. como em “%hhd”. Neste caso, o printf esperará ver um char no parâmetro correspondente. O modificador “h” aplica-se ao tipo short e o modificador “ll” ao tipo “long long int”. A mesma coisa com “%u”, mas lembre-se que os tipos serão interpretados como unsigned.

Outro modificador útil é o usado para especificar valores expressos nos tipos size_t ou ssize_t. Esse modificador é o ‘z’, como em “%zu” (para size_t) e “%zd” (para ssize_t).

Tamanho e precisão:

O tamanho especificado em width, se houver um, é o tamanho mínimo, em caracteres, do campo. Se a string convertida tiver menos caracteres que o especificado neste campo, então ela será alinhada à esquerda e preenchida com espaços. Assim, ao usarmos a marca “%20s” e o ponteiro apontar para uma string de 4 caracteres, os 16 caracteres seguintes serão preenchidos com espaços. Note que o campo não especifica o tamanho máximo…

O campo “precisão” (precision) depende da marca… No caso de marcas para ponto flutuante, ela especifica o número máximo de “casas depois da ‘vírgula'” que serão impressas. No caso das marcas de tipo inteiro (“%d”, ‘%i”, “%u”, “%o” ou “%x”), especifica o número máximo de algarismos que serão impressos e, no caso da marca “%s”, o máximo de caracteres.

Este último caso pode ser confuso. Uma marca “%20.10s”, por exemplo, nos diz que, no mínimo, 20 caracteres serão impressos, mas o valor terá, no máximo, 10…

Existe apenas uma diferença para as marcas do tipo ponto flutuante: Para as marcas “%g”, a precisão refere-se ao número de dígitos significativos (a quantidade de algarismos).

Flags:

Já que o campo width nos dá o tamanho mínimo e preenche a sub string com espaços à direita se ela for menor que o valor especificado, podemos usar o flag “-“, como em “%-20s” para dizermos ao printf que ele deve fazer um alinhamento à direita, ou seja, preencher com espaços o lado esquerdo da string.

O flag “+” nos diz, para os tipos numéricos, que ‘+’ ou ‘-‘ sejam impressos, precedendo o valor.

Um simples espaço usado no campo flags diz que ‘-‘ deve ser impresso se o valor for negativo, mas ‘+’ não deve, colocando um espaço no lugar.

O flag ‘0’ pode ser usado para preenchimento com zeros, ao invés de espaços, se a sub string não tiver o tamanho mínimo especificado em width. Por exemplo, “%08x” imprimirá um valor hexadecimal de exatamente 8 algarismos, mesmo que tenha que colocar zeros à esquerda.

Existem 3 outros flags especiais: ‘#’ denota a forma alternativa da marca… Para a marca “%x” a sequência “0x” será impressa antes do valor, como em “%#08x”… Isso imprimirá, por exemplo, um valor “0x0000ffab”; Uma aspa simples “‘” é o flag usado para imprimir os agrupamentos de milhares. Isso depende da configuração de localidade (locale), mas, por exemplo, uma marca “%’.2f” onde o valor 123456.78 for passado como argumento resultará na sub string “123,456.78”, se estivermos usando um locale en-us.

O terceiro flag especial é um indicador do argumento. Isso é uma extensão do GCC e não faz parte do padrão C99 (mas faz parte do padrão POSIX!). Suponha que queiramos imprimir o mesmo valor de uma variável x três vezes no mesmo printf. Poderíamos fazer algo assim:

printf("x = %d (Hexa: %#x, Octal: %#o\n", x, x, x);

Mas o GCC permite dizermos ao printf qual argumento queremos que ele use na marca usando um flag “n$”, onde $n \ge 1$. O valor de n nos dá a posição do argumento na lista e podemos escrever a mesma chamada, acima, assim:

printf("x = %1$d (Hexa: %1$#x, Octal: %1$#o\n", x);

A desvantagem é que todas as marcas devem ter o indicador de argumento.

O retorno de printf:

A função printf tem o seguinte protótipo:

int printf(char *fmt, ...);

Ela retorna um inteiro contendo a quantidade de caracteres impressa ou um valor negativo se houve um erro.

Como funciona a aritmética fundamental em ponto flutuante

Quando falamos em aritmética fundamental usando valores inteiros (adição, subtração, multiplicação e divisão) acredito que não há grandes dúvidas, quando os argumentos estão expressos em formato binário. Mas, e quanto à codificação em ponto flutuante que citei em artigos anteriores?

Adição e subtração

O problema que enfrentamos é que, além dos bits significativos, temos o escalonamento (ou o “deslocamento” do ponto). Na adição precisamos que ambos os valores tenham a mesma escala para poder adicioná-los. Um exemplo, em decimal, é a tentativa de adicionar 1.0\cdot10^0 ao valor 1.0\cdot10^2. Não podemos, simplesmente, somar 1.0 com 1.0! Temos que tornar as escalas (10^0 e 10^2) iguais e, para isso, adaptamos o menor valor:

\displaystyle 1.0\cdot10^2 + 0.01\cdot10^2=1.01\cdot10^0

Aqui, 1.0\cdot10^0 foi transformado em 0.01\cdot10^2. O motivo de adequarmos o menor valor é que os bits do menor valor serão deslocados para a direita e a adição tenderá a obter uma soma normalizada. Troque agora o valor normalizado por um valor inteiro binário (com o bit 23, implícito, setado) e a escala na base 2, ao invés de 10… Lembre-se que, no artigo anterior, demonstrei que o valor contido na “fração” da estrutura de um float é o valor inteiro do numerador, cujo denominador é sempre o mesmo, 2^{23}.

Embora esteja mostrando o funcionamento do ponto de vista de um valor fracionário, o coprocessador realiza a adição com o valor inteiro. Usando o programinha shwflt, do artigo anterior, temos:

$ ./shwflt 1e0
[Normal] 1 -> s=+, E=0, f=0 (0b1.00000000000000000000000)
$ ./shwflt 1e2
[Normal] 100 -> s=+, E=6, f=0x480000 (0b1.10010000000000000000000)

Consideremos a=1.0\cdot10^2 e b=1.0\cdot10^0. Com o procedimento descrito acima, temos que a>b e, portanto, b deve ser adequado. Em binário, considerando apenas as frações e o bit implícito, temos, em binário, a=0b110010000000000000000000 e b=0b100000000000000000000000, mas b deve ser deslocado 6 bits para a direita para que os expoentes sejam equalizados (E_a=E_b) e, portanto, b=0b000000100000000000000000. A adição das frações, então, dá a+b=0b110010100000000000000000 e o resultado final, na estrutura do float será f=0b10010100000000000000000,\,E=6, como pode ser visto aqui:

$ ./shwflt 1.01e2
[Normal] 101 -> s=+, E=6, f=0x4a0000 (0b1.10010100000000000000000)

Existe um detalhe interessante: Se a diferença entre os expoentes E_a e E_b for maior que 24, significa que o menor valor pode ser seguramente desconsiderado na adição. O motivo, óbvio, é que ele deverá ser deslocado 24 bits para a direita, fazendo com que todos os seus bits significativos “desapareçam”. Assim, a adição só é feita se E_a - E_b < 24 onde E_a > E_b.

A subtração segue as mesmas regras, mas a ordem com a qual os operandos aparecem é importante.

Renormalização

Consideremos o caso de adicionarmos o valor fracionário, binário, 0b1.0 a ele mesmo. Teremos que obter o valor 2.0 ou 0b10.0. No entanto o bit MSB é implícito e precisa estar isolado da parte fracionária. Por isso, na adição, precisamos ter espaço suficiente para o armazenamento para um bit extra à esquerda, no valor inteiro. Ou seja, o resultado por ter 25 bits de tamanho: Assim, se R=0x800000+0x800000=0x1000000, o valor de R precisa ser deslocado 1 bit para a direita (R\text{ shr }1 e o expoente E é incrementado. Vejamos:

$ ./shwflt 2
[Normal] 2 -> s=+, E=1, f=0 (0b1.00000000000000000000000)

Repare que f=0, do mesmo jeito que acontece com o valor 1.0, mas o expoente E=1 por causa da renormalização.

Da mesma forma, isso também pode ocorrer com a subtração. O MSB sempre tem que ser igual a 0b1, a não ser que obtenhamos um valor subnormal. Se o MSB obtido for zero o valor final será deslocado para a esquerda tantos bits quantos forem necessários para que ele seja igual a 0b1, mas o expoente E, do resultado, será subtraído dessa mesma quantidade de bits, normalizando o valor. Se o coprocessador perceber que E será menor que -126, então o valor é subnormal e a renormalização não é feita.

Multiplicação e divisão

Diferente da adição (e da subtração), a multiplicação não precisa sofrer adequações das frações. Isso fica claro quando você percebe que basta somar os dois expoentes de mesma base para obter a escala correta e, as frações, só precisam ser multiplicadas:

\displaystyle (f_a\cdot2^{E_a})\cdot(f_b\cdot2^{E_b}) = (f_a\cdot f_b)\cdot2^{E_a + E_b}

O detalhe é que, ao obtermos a fração com o bit implícito, teremos um valor de 24 bits de tamanho que, ao multiplicar por outro valor de 24 bits de tamanho nos dará um valor de 48 bits de tamanho. Considere o caso de multiplicarmos 1.0 por ele mesmo. Esse valor é codificado, em hexadecimal, como 0x800000. Depois da multiplicação obtemos 0x400000000000. Não é óbvio, mas o resultado encontra-se 23 bits deslocado para a esquerda, ou seja, justamente os 23 bits da parte fracionária dos argumentos originais! A resposta é justamente \left[(a\cdot b)\text{ shr }23\right]\cdot2^{E_a+E_b}, onde a e b são os valores inteiros de 24 bits com o bit implícito!

A divisão, é claro, segue o sentido inverso. os bits significativos devem ser divididos e os expoentes subtraídos, mas diferente da multiplicação, o dividendo deve ser deslocado 23 bits para a esquerda antes que a divisão possa ser feita. Novamente, vamos tentar dividir 1.0 por ele mesmo… Se dividirmos 0x800000 por ele mesmo obteremos o quociente 1. Mas 1 é apenas o bit 0 da fração, ou seja, 2^{-23}. Mas, se deslocarmos a para esquerda em 23 bits, teremos \frac{0x400000000000}{0x800000}=0x800000. Assim, a divisão binária fica \frac{a\text{ shl }23}{b}\cdot2^{E_a-E_b}. Claro que temos que considerar o caso espacial onde $label b=0$.

Atenção que a renormalização também pode ser necessária depois de multiplicações e divisões. Tomemos o caso onde todos os bits significativos estejam setados e E=0. Se quisermos elevar esse valor ao quadrado, neste caso, a será 0xffffff e obteremos 0xfffffe000001. Ao deslocarmos 23 bits para a direita teremos 0x1fffffc, que tem 25 bits de tamanho! Isso deve ser deslocado em 1 bit para a direita e o valor de E incrementado. De fato, o programinha abaixo atesta esse fato:

#include <stdio.h>

union flt_u {
  float v;
  struct {
    unsigned int f:23;
    unsigned int e:8;
    unsigned int s:1;
  };
};

void main(void)
{
  /* Todos os bits significativos setados.
     E=0. 
     s=+ */
  union flt_u flt = { f:~0, e:127 };

  float b;

  b = flt.v * flt.v;

  printf("a = %.18f, a*a = %.18f, "
         "f[a*a]=%#x, E[a*a]=%d\n",
    flt.v,
    b,
    (*(union flt_u *)&b).f,
    (*(union flt_u *)&b).e-127);
}

Ao executar o código acima, temos:

a = 1.999999880790710449, a*a = 3.999999523162841797, f[a*a]=0x7ffffe, E[a*a]=1

Como os dois valores originais tẽm expoente zerado, o expoente final deveria ser zerado também (E_a+E_b=0+0=0, mas a renormalização nos deu um expoente unitário, sendo fácil perceber que a parte fracionária é resultante do shift para a direita do valor que expus anteriormente. Na divisão algo similar pode acontecer, mas no sentido contrário.

Underflows. overflows e NaNs

O que acontece se tivermos f_a com todos os bits setados e E_a=127 (o maior expoente possível em base 2), pegarmos esse valor máximo e adicionarmos 2^{104}? O valor 2^{104} é precisamente o bit 0 da estrutura do float com um expoente 127. O que ocorrerá que o coprocessador fará o cálculo, mas obterá um valor “estranho” com expoente 128 que, por definição do padrão, corresponde a ∞ ou a NaN. No caso da adição, ao ocorrer um overflow termos o resultado ±∞, dependendo do sinal. Mas, divisões podem causar resultados NaN. É o caso da divisão \frac{0}{0}.

Ao obter, no resultado, um expoente maior que 127 ou menor que -127 (para valores subnormais) o coprocessador decide o que vai deixar na fração… Se o valor for “infinito”, a fração será zerada. Se for um NaN, geralmente temos algum “lixo” armazenado por lá… O bit 22 será 0 se tivermos um qNaN e 1 se tivermos um sNaN, mas não podemos usar os demais bits para qualquer coisa, senão forçarmos um NaN (para o caso do qNaN).

Parece “ponto fixo”, não é?

Não sei se escrevi algo sobre ponto fixo neste blog (espero que sim, me avisem caso contrário, ok?), mas esse esquema de usarmos os bits significativos como se fossem valores inteiros lembra muito a maneira como ponto fixo funciona, com uma grande vantagem: Se dividirmos os 32 bits em partes iguais entre o componente inteiro e o fracionário poderemos, em ponto fixo, expressar o valor mínimo de 2^{-16} e o máximo de \frac{2^{32} - 1}{2^{16}}. Ponto flutuante, do mesmo tamanho, em contrapartida, nos garante a faixa entre o valor mínimo subnormal de 2^{-149}\approx1.4\cdot10^{-45} até o máximo de \frac{2^{25} - 1}{2^{23}}\cdot2^{127}\approx6.8\cdot10^{38}. Uma faixa bem maior e com maior precisão usando menos bits significativos (apenas 24).

Note que, para ser exato, os 32 bits de um ponto fixo deveriam ser divididos em 31 bits significativos e um de sinal. E, no neste caso, é comum usarmos o bit de sinal para conter valores inteiros em complemento 2, coisa que não é feita com ponto flutuante. Os bits significativos, neste último caso, são sempre armazenados de forma positiva. Fica a cargo dos algoritmos determinar se, por exemplo, estamos realizando operações com valores com sinais iguais ou diferentes, o que adiciona alguma complexidade (pouca!). E, é claro, o padrão IEEE 754 ainda fornece alguns valores especiais, o que não é diretamente possível com ponto fixo.

Mais alguns detalhes sobre ponto flutuante

Tenho escrito sobre ponto flutuante aqui para alertar aos desavisados de que realizar cálculos com esses tipos não é a mesma coisa que lidar com números “reais”, na matemática tradicional. Realmente, espero que esses textos criem a consciência de que nem tudo é tão simples quanto parece.

Neste texto vou tentar esmiuçar alguns detalhes adicionais sobre a estrutura do tipo float, usado na linguagem C ou, mais precisamente, do tipo single precision, como especificado no padrão IEEE 754. A discussão, claro, aplica-se aos outros tipos (double precision e extended double precision, bem como às “novos” tipos descritos na revisão de 2008 da referida especificação).

Lembrando sobre a estrutura de um float:

O tipo float, ou single precision, tem 32 bits de tamanho e a seguinte estrutura:

Estrutura de um float

O valor decimal, normalizado, codificado nessa estrutura, é obtido através da equação abaixo:

\displaystyle v=(-1)^s\cdot\left(1+f\right)\cdot2^e

Onde s corresponde ao bit de sinal (0 significa + e 1, -); o valor f é uma “fração”, ou seja, um valor entre 0 e 1 (0\leqslant f<1). Repare que ao valor de f adicionamos o valor 1 que não está presente na estrutura acima. Esse valor é implícito e segue a regra daquilo que é conhecido como notação científica, onde o primeiro algarismo significativo deve sempre maior que zero e ser o único algarismo “inteiro”. No caso de valores binários não temos alternativa a não ser usarmos o algarismo 1.

O componente e é o expoente do escalonamento que indica para que lado o “ponto” flutuará. Valores positivos fazem o ponto flutuar para a direita e negativos, para a esquerda. O expoente e merece alguma atenção, já que ele é codificado de forma “polarizada” (biased), onde o valor central, zero, com os 8 bits disponíveis, corresponde a 127 (ou 0b01111111, em binário). Daqui por diante usarei nomenclatura maiúscula para expressar o expoente “real” da escala, de acordo com a equação: E=e-127 e quando vir o expoente e, minúsculo, trata-se do valor armazenado na estrutura do float.

Porque chamamos os bits significativos de “fração”?

Era comum vermos, na literatura especializada, o fragmento (1+f) ser chamado de “mantissa”, mas essa nomenclatura está errada. Não porque o uso do termo seja mais comum com logaritmos, mas porque ela significa “a parte quebrada ou excedente”, ou seja, justamente a parte fracionária, excluindo o componente inteiro. Isso poderia ser aplicado apenas ao compoennte f, mas é preferível chamar (1+f) de bits significativos e o componente f de “fração”.

O termo “fração” é apropriado. Como o valor expresso na estrutura é binário, podemos simplesmente “deslocar a vírgula” 23 bits para a direita e obtermos um valor inteiro. Isso quer dizer que o valor expresso nos bits significativos é, de facto, um número inteiro que compõe uma fração somada a uma unidade:

v=(-1)^s\cdot\left(1 + \frac{n}{2^{23}}\right)\cdot2^E

O valor 2^{23} no denominador vem do fato de que f sempre será menor que 1 e essa é quantidade máxima de arranjos binários possíveis para esse campo.

Como a fração é obtida?

Graças ao MSB, implícito nos bits significativos, o valor inteiro 1.0 é expresso simplesmente como (1+0), mas se fossemos usar todos os 24 bits significativos ele seria expresso como 0x800000 ou 8388608. Se queremos converter um valor d, decimal, para a base binária (num valor b) que contenha o MSB implícito, podemos fazer:

\displaystyle f=\frac{n}{2^{23}}\,\therefore\,n=f\cdot2^{23}

Vou usar o programinha abaixo para mostrar a estrutura de um float. Isso nos auxiliará a corroborar a alegação acima.

/* shwflt.c */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

/* Usada para decompor um float
   em seus componentes. */
union float_u {
  float v;
  struct {
    unsigned int f:23;
    unsigned int e:8;
    unsigned int s:1;
  };
};

/* Retorna string com os 23 
   bits em binário. */
static char *binfrac(unsigned int f)
{
  static char bits[24];
  char *p;
  int i;

  p = bits;
  i = 23;
  while (i--)
  {
    /* Isola o bit 22 */
    *p++ = (f & (1U << 22)) ? '1' : '0';
    f <<= 1;
  }

  /* fim da string. */
  *p = 0;

  return bits;
}

int main(int argc, char *argv[])
{
  union float_u flt;
  char sign;

  /* Precisamos de 1 argumento */
  if (!*++argv)
  {
    puts("Usage:\n"
         "  shwflt <value>");
    return 1;
  }

  errno = 0;
  flt.v = strtof(*argv, NULL);
  if (errno == ERANGE)
  {
    fprintf(stderr, 
            "ERROR: Invalid value: '%s'\n",
            *argv);
    return 1;
  }

  sign = flt.s ? '-' : '+';

  /* NaN ou Infinito? */
  if (flt.e == 255)
  {
    if (!flt.f)
      printf("%c\xe2\x88\x9e", sign); /* utf-8 */
    else
      printf("%cNaN\n", sign);

    return 0;
  }

  /* Valor normalizado? */
  if (flt.e)
  {
    printf("[Normal] %.8g -> "
           "s=%c, E=%d, f=%#x (0b1.%s)\n",
      flt.v,
      sign,
      flt.e-127,
      flt.f,
      binfrac(flt.f));
  }
  else
  {
    printf("[SubNormal] %.8g -> "
           "s=%c, E=-126, f=%#x (0b0.%s)\n",
      flt.v,
      sign,
      flt.f,
      binfrac(flt.f));
  }

  return 0;
}

Tomemos o valor 1.0 como exemplo: Temos b=1.0\cdot2^{23} ou, em binário, 0b100000000000000000000000. Esse número binário tem exatamente 24 bits de tamanho na sua parte inteira e, então, não há necessidade de escalonar a fração (deslocar o “ponto”, lembra?):

./shwflt 1
[Normal] 1 -> s=+, E=0, f=0 (0b1.00000000000000000000000)

Note que O 24º bit é, como comentei antes, implícito e, portanto, não aparece no componente f. Ele é indicado aqui como “0b1.”. Outro ponto digno de nota é que estou multiplicando o valor decimal “real” por 2^{23}, ao invés de apenas a fração. Isso pode ser feito porque, de qualquer maneira, vamos descartar o 24º bit.

Consideremos, agora, o valor 0.1: Temos 0.1\cdot2^{23} ou, em binário, 0b11001100110011001100.11, que tem apenas 20 bits na parte inteira. Esse valor, normalizado e limitado a 24 bits, acabará sendo 0b1.10011001100110011001100_1. O bit extra, mais à direita (depois do separador ‘_’), é chamado de bit de guarda, usado no arredondamento. Ele deve ser somado ao LSB do valor binário normalizado (é o equivalente a somar 0,5 a um valor com “uma casa decimal” para obter o valor inteiro mais próximo). Assim, o valor de f será 0b10011001100110011001101 ou, em hexadecimal, 0x4ccccd.

$ ./shwflt 0.1
[Normal] 0.1 -> s=+, E=-4, f=0x4ccccd (0b1.10011001100110011001101)

Voltando ao fato do valor binário obtido ter 20 bits na sua parte inteira, isso significa que temos que deslocar o ponto 4 bits para a direita. Ou seja, o valor real é 0b0.000110011001100110011001100…. Temos, então E=-4, aplicado ao valor normalizado. Então, a estrutura do valor 0.1 num float nos dá: v=(-1)^0\cdot\left(1+\frac{5033165}{8388608}\right)\cdot2^{-4}\approx0.1.

Outro exemplo: Vamos ver como fica o valor aproximado de π (3.141593):

./shwflt 3.141593
[Normal] 3.141593 -> s=+, E=1, f=0x490fdc (0b1.10010010000111111011100)

De acordo com nosso esquema, n=3.141593\cdot2^{23}, ou seja:

0b1100100100001111110111000.00101100001010111101

A parte inteira tem 25 bits e os primeiros 24 podem ser normalizados (e arredondados), obtendo 0b1.10010010000111111011100. Além disso, se temos 25 bits no valor original, isso significa que precisaremos adicionar 1 bit extra à esquerda do valor inteiro. Assim, E=1 e f=0x499fdc, exatamente como esperávamos. Isso nos dá v=(-1)^0\cdot\left(1+\frac{4788188}{8388608}\right)\cdot2^1\approx3.141593.

Minha trapaça e o que falta…

Como consegui obter a representação fracionária dos valores decimais em binário? Eu trapaceei… Fiz uso do Big number Calculator (bc):

$ bc
obase=2

0.1*(2^23)
11001100110011001100.1100

10*(2^23)
101000000000000000000000000

3.141593*(2^23)
1100100100001111110111000.00101100001010111101

A multiplicação por 2^{23} serve apenas para obtermos o valor inteiro que será colocado no componente f do valor em ponto flutuante, para o tipo float, onde devemos excluir o MSB.

Isso é uma “trapaça” no sentido de que estou usando um software que converte o valor fracionário corretamente para nós. Na prática, não deveria usar esse tipo de auxílio e te mostrar como os valores fracionários são obtidos através de operações elementares usando valores inteiros apenas (suponha que o processador não tenha uma unidade de ponto flutuante, como é o caso dos antigos 386).

Sinto dizer, mas existe outra trapaça ai… O valor binário obtido tem tamanho arbitrário e, na prática, temos apenas uma quantidade de bits limitada para trabalhar. Nos processadores Intel modernos temos registradores de até 64 bits de tamanho e, se usarmos AVX, podemos chegar até 256 bits. Lembre-se que o valor binário final pode ser escalonado até 2^{126}, ou seja, além dos 24 bits significativos, podemos ter até 127 bits adicionais, totalizando 151 bits (para valores normalizados)… E olha que estou falando apenas do tipo float… Com o tipo double, que tem 53 bits significativos e o expoente E pode chegar até 1023, o que nos dá um total de 1080 bits. Muito além até mesmo do AVX-512, que não está disponível em todos os processadores modernos.

Isso significa que aritmética de múltipla precisão não é usada nem mesmo pelo co-processador. As rotinas têm que levar em conta o tamanho dos registradores da arquitetura em questão e, ainda assim, serem o mais rápidas possíveis… Não discuti neste artigo como essas rotinas funcionam, mas o farei, em breve…

Ambientes gráficos – Não é tão simples como você pensa!

Um leitor amigo me pediu para falar um cadinho sobre ambientes gráficos. Infelizmente o assunto é tão extenso que é meio difícil cobrir aqui. Pretendo mostrar, superficialmente, uma técnica usada ma maioria das GUIs…

Já reparou como suas janelas são desenhadas na tela? Observe isso:

01-2-windows

Para os propósitos deste texto, esqueça os efeitos especiais como a sombra projetada da janela do terminal sobre a janela do browser e de ambas as janelas sobre o desktop.

A janela do browser, embaixo da do terminal, é parcialmente desenhada. O pedaço onde o terminal está não pode aparecer para o usuário (a não ser que você seja um daqueles que goste de usar terminais “transparentes”). No caso de janelas com conteúdos estáticos isso pode parecer bem simples de fazer. Mas, e quanto a vídeos?

Nada de dstrações com a beleza da moça!
Nada de distrações com a beleza da moça!

Vídeos são apenas sequências de imagens desenhadas em intervalos regulares para causar a sensação de movimento. No caso do vídeo da moça acima (linda, não?), o mesmo pedaço da janela, à esquerda, não é tocado pelas imagens que compõem o vídeo… Como isso é feito?

A GUI divide a janela sobreposta em “sub janelas” ou regiões. Ele faz isso porque só sabe manipular áreas retangulares e, então, a divisão em regiões da janela do vídeo fica assim:

grab3-2-regions

No exemplo acima usei as cores verde e azul para exemplificar a divisão da janela em regiões… Todos os pixels que não estão nessas áreas simplesmente não são desenhados. Mas, como fica quando temos várias janelas sobrepostas? A GUI monta uma lista de regiões retangulares, por exemplo:

03-3-regions

Aqui temos 3 “regiões” (verde, azul e vermelha).

Obviamente que quanto mais janelas na tela, mais complexa tende a ficar. Especialmente se algum efeito especial é usado (sombras e janelas não retangulares, por exemplo). Sem contar que, às vezes, teremos que lidar com casos onde a janela é subdividida em diversas regiões de apenas 1 pixel…

Hoje em dia é mais provável que as GUIs usem stencil buffers para mascarar os bits que não serão plotados. No caso do Linux, por exemplo, os ambientes X11 modernos costumam usar OpenGL para esse fim. É provável que cada janela tenha seu próprio stencil buffer e o sistema mantenha uma lista das janelas visíveis e suas coordenadas z (quem está na frente de quem). Assim, para cada janela, as janelas com z>z_{corrente} são desenhadas no stencil buffer da janela em questão (apenas os contornos e preenchidas de branco), criando a máscara.

Assim, quando a GUI for, de fato, desenhar a janela, basta aplicar o stencil… É como serigrafia: As áreas mascaradas não deixam passar a tinta… Isso evita a complicação de manter uma lista de regiões. E, como raster operations são o default nas placas de vídeo atuais, é pouco provável que existam problemas com processamento ao lidarmos com áreas grandes ou com múltiplas subdivisões.