SDK
Como o Miblo anima uma tela com 80 KB de RAM
A memória do Miblo: o que é fixo, o que uma cena custa, os pisos de 8 e 12 KB, o que é emprestado à rede e como ler o diagnóstico quando uma cena não carrega.
O Miblo tem 80 KB de RAM para tudo: o firmware, o Wi-Fi, as suas sessões, o pet, o card e a cena do app na tela. Não há frame buffer (uma imagem da tela ocuparia 115.200 bytes) e não há memória virtual: o que não cabe não roda. Esta página diz onde essa memória vai, o que uma cena custa, o que o Miblo empresta à rede e como ler o diagnóstico quando uma cena não carrega. O guia de animação (Animações na tela do Miblo) explica como animar; aqui está a conta por trás dele.
O orçamento
Dos 80 KB, o firmware ocupa uma parte fixa, que existe antes de qualquer conexão: 51,3 KB no Miblo 1.28.1 (eram 54,8 KB no 1.28.0). O resto é o heap, de onde saem os buffers do Wi-Fi, cada conexão aberta, cada requisição da ponte enquanto é lida, o card do app, os buffers do pet, as capturas dos itens e a cena. Num Miblo pareado, com um card e a stream da ponte ligados, sobram uns 17 a 19 KB livres entre uma requisição e outra (medido no Amon). É dentro desses KB que uma cena precisa caber, ao lado dos pisos abaixo.
Três números organizam tudo:
| Número | O que é |
|---|---|
| 12 KB | A reserva da rede. Uma requisição que precisa de N bytes (o corpo, o documento JSON da resposta) só é atendida se sobrarem N + 12 KB livres, num bloco inteiro de N + 4 KB; senão a resposta é 503 busy. É o que mantém o Wi-Fi vivo |
| 8 KB | O piso do player. Uma cena carrega quando a sua necessidade + 8 KB estão livres (num bloco de necessidade + 4 KB), e devolve a memória quando o heap cai abaixo de 8 KB depois de uma requisição |
| 16 KB | A reserva das camadas comuns de desenho fora da tela (um item mesh preenchido, por exemplo). As faixas de composição dos itens não usam essa reserva: usam o piso de 8 KB |
O que é fixo
Dentro dos 51,3 KB estão o estado do firmware, as fontes e os desenhos que ficam na RAM, os limites das sessões, as filas de alerta e os formatos próprios. Desde o 1.28.1 três coisas que eram fixas passaram a seguir o uso:
Os textos das sessões. Os nomes, ferramentas, modelos e detalhes das linhas do snapshot ficavam num bloco fixo de 1.536 bytes. Agora ficam num pool do tamanho do conteúdo, com cada texto repetido guardado uma vez: 20 sessões típicas usam 234 bytes. O teto continua 1.536 bytes (um snapshot maior que isso deixa as últimas linhas sem texto, nunca um texto cortado). São 2,4 KB a mais livres.
Os buffers do pet. Um pet bitmap (MPET1) desenha a partir de um quadro de 1.152 bytes; um pet vetorial (MPET2) usa tabelas de rascunho. O Miblo devolve esses buffers sempre que o pet não está na tela (a tela App, a Visão geral, um alerta de app, a tela apagada) e quando o formato do pet muda; o logo e a identidade ficam, e o próximo desenho do pet recarrega o que precisa.
As constantes. Alguns formatos próprios foram para a flash.
O card do app, quando há um, ocupa até 1 KB no heap enquanto está na tela.
O que uma cena custa
Uma cena de tiles (o caminho de screen.animate com arte que cabe em 256 tiles, com ou sem efeito por linha) custa, na RAM, três partes:
| Parte | Bytes |
|---|---|
| Os tiles residentes | o tamanho compactado do conjunto (abaixo) |
| O player | 3.104 |
| O cache de paletas | 528 |
Os tiles são a parte que varia. No arquivo cada tile tem 32 bytes (64 pixels de 4 bits). Na RAM o Miblo guarda cada um do jeito mais curto que reproduz exatamente os seus pixels, decidido ao carregar a cena:
| Cores no tile | Como fica | Bytes |
|---|---|---|
| 1 | uma constante | 1 |
| 2 | dois índices e 1 bit por pixel | 9 |
| 3 ou 4 | quatro índices e 2 bits por pixel | 18 |
| 5 a 16 | os 4 bits por pixel do arquivo | 32 |
Mais 2 bytes de índice por tile. A forma compacta só é usada quando o total, com o índice, fica menor que os 32 bytes × tiles do arquivo; nenhuma cor é descartada, nada muda no arquivo nem no SDK, e o desenho continua lendo da RAM, nunca da flash. O Radar, com 237 tiles, caiu de 7.584 para 3.816 bytes de tiles: a cena inteira, de 11.724 para 7.448 bytes. Com o piso de 8 KB, ela carrega quando há uns 15,6 KB livres num bloco de 11,5 KB. Uma cena pequena (poucos tiles, cores chapadas) custa 4 a 5 KB e carrega com uns 12 a 13 KB livres.
O que ajuda, na arte: menos tiles diferentes e menos cores por tile. Um fundo chapado é um byte por tile; um degradê é um tile de 32 bytes em cada casa. As regras do capítulo 4 do guia valem aqui pelo mesmo motivo que valem para a velocidade.
O que ajuda, no fps: nada. A taxa não muda a memória de uma cena. O player lê o mapa de cada passo da flash em faixas de 512 bytes; um passo que só move sprites nem lê o mapa de novo.
Itens sobre a animação
O card é desenhado por cima da cena. Um item parado (big, text ou bar sem fx e sem live) é composto na memória com os pixels da animação, faixa por faixa de 240x16 pixels e só nas linhas onde ele tem tinta, e vai ao painel de uma vez: o painel nunca recebe o fundo sem as letras. Vale na primeira vez que o item aparece, quando o valor muda e a cada quadro que se move por baixo dele.
A memória disso:
Uma faixa de 240x16 a 4 bits pede 1.953 bytes. Ela é admitida com o piso de 8 KB (não com a reserva de 16 KB das camadas comuns), então cabe ao lado de uma cena carregada.
Quando sobra memória, o Miblo guarda a captura de cada item parado (até 6 KB para todos, tirados só do que sobra acima de 12 KB + 4 KB) e compõe sem redesenhar o item. Quando não sobra, compõe com uma faixa temporária e a devolve antes de a rede precisar dela.
As capturas são a primeira memória que o Miblo empresta, antes da cena.
Um item com fx, um ring, um spark ou um mesh não passa por aí: a cada passo a caixa dele é redesenhada inteira (primeiro os pixels da animação, depois o item). Por isso o guia pede itens pequenos e poucos efeitos sobre a arte.
O que é emprestado, e quando
Uma cena é um cache da flash: o Miblo pode soltá-la a qualquer momento e carregá-la de novo onde estava, no mesmo passo, com o relógio do efeito seguindo. Enquanto a memória está emprestada, a imagem da cena fica no vidro (não há frame buffer para apagar), e os itens continuam sendo desenhados do jeito antigo.
A ordem dos empréstimos, para uma requisição que não cabe com a reserva de 12 KB:
As capturas dos itens vão primeiro (o orçamento delas vai a zero).
A cena vai em seguida, se a requisição pede mais de 2 KB, ou se, pedindo menos, não cabe ao lado dela com 8 KB sobrando. Uma requisição pequena (um poll, um documento de 1 KB) roda ao lado da cena.
E o piso: depois de uma requisição, se sobram menos de 8 KB com a cena carregada, as capturas e depois a cena são devolvidas para a rede.
A volta:
Nunca enquanto uma requisição está sendo lida.
Depois de uma espera: 150 ms após um empréstimo a uma requisição, 2 s após o piso.
Quando a necessidade da cena + 8 KB estão livres (as capturas contam como livres: vão antes) num bloco inteiro de necessidade + 4 KB. Uma falta nesse instante é tentada de novo 250 ms depois; um arquivo que não abre ou um player que falha, 5 s depois.
Só com a tela App no painel. Três segundos depois de a tela App sair, a memória da cena é devolvida de vez.
POST /api/screen/anim-play só responde 503 busy quando a cena não caberia nem depois de a requisição acabar (necessidade + 8 KB maior que o livre + o que é devolvível + 4 KB). Senão ela é aceita e carrega quando o heap tiver lugar.
Como ler o diagnóstico
Tudo abaixo é lido no aparelho, com o token do computador pareado (Authorization: Bearer), ou pela ponte onde dito.
GET /api/info
| Campo | O que é |
|---|---|
heap | O heap livre no instante da resposta, dentro do handler, depois de ele alocar o próprio documento (~2 KB). Não compare com idleHeap |
idleHeap | O heap livre amostrado pelo laço principal, fora de qualquer requisição. É o número para comparar entre leituras e entre versões |
maxBlock | O maior bloco inteiro livre. Muito menor que idleHeap é fragmentação |
minHeapParse | O menor heap livre visto durante a leitura de um snapshot |
petDrawingBytes, guestDrawingBytes | Os buffers de desenho do seu pet e do pet visitante, se estão carregados (0 quando devolvidos) |
petLogoBytes | O logo do pet personalizado, que fica |
snapshotTextBytes, snapshotTextUsed | O pool dos textos das sessões: o alocado e o usado |
GET /api/screen
| Campo | O que é |
|---|---|
panel | O que está no vidro agora: app, main, desk, summary, alert, pet, limits, preview, boot, setup, code, disconnected, updating, off (tela apagada) ou other. shown diz só se a tela App está fixada; não diz se ela está no vidro |
anim.loaded | A cena está carregada na RAM |
anim.yielded | Não está porque a memória dela foi emprestada; ela volta sozinha |
anim.need | O que carregar custa: tiles compactados + player + cache (quando não está carregada) |
anim.why | Enquanto não está carregada: quantas passagens do laço foram recusadas por cada motivo desde a última carga. notShown (a tela App não está no painel), pending (uma requisição sendo lida), hold (a espera depois de um empréstimo), free (faltou need + 8 KB livres; lastFree diz quanto havia), room (faltou um bloco inteiro; lastFree e lastBlock), failed (o arquivo ou o player) |
anim.yields, anim.yieldNeed | Quantas vezes a memória da cena foi emprestada a uma requisição desde que começou a tocar, e o que a última pediu |
anim.floorYields | Quantas vezes foi devolvida pelo piso de 8 KB |
anim.compose | ok: faixas compostas sobre a animação; noLayer: faixas que não couberam na memória naquele instante (o item foi redesenhado do jeito antigo); inexact: capturas que não representaram o item com exatidão (idem). ok conta faixas, não quadros, e um item desenhado a partir da captura guardada não conta |
anim.heap, anim.tileBytes, anim.tileRawBytes | Com a cena carregada: o que ela ocupa, os tiles residentes compactados e o que seriam sem compactar |
anim.real | { fps, drawMs }: a taxa alcançada e o último desenho |
governor | Cada tipo que anima (anim, fx, meshWire, meshFlat, transition, live): o fps atual, o último desenho e a média |
miblo screen status
A ponte lê loaded, yielded, why e yields do aparelho e imprime um motivo na linha da animação:
Animação: blocos de Clima · 8 fps pedidos · — fps real · — ms · não carregada (sem memória livre: 7,9 KB livres).
Animação: blocos de Radar · 10 fps pedidos · 9 fps real · 48 ms · memória emprestada 3×.panel, floorYields e compose não passam pela ponte: leia-os no aparelho.
Por que a cena não carregou
miblo screen status diz | anim.why | O que está acontecendo | O que fazer |
|---|---|---|---|
| "a tela do app não está no Miblo" | notShown | A tela App não está no painel (outra tela, a rotação em outro app, a tela apagada). A cena só carrega com ela no vidro | Nada: ela carrega na vez do app. Confira panel |
| "uma requisição em curso" | pending | A ponte está mandando algo (um card, um lote ao vivo) a cada passagem | Normal por instantes; constante, é uma ponte mandando demais (um card a cada segundo, por exemplo) |
| "devolvendo a memória emprestada" | hold | A espera de 150 ms ou 2 s depois de um empréstimo | Nada |
| "sem memória livre: N KB livres" | free | Faltou need + 8 KB. lastFree diz quanto havia | Uma cena menor (menos tiles diferentes, menos cores por tile); menos coisas abertas no Miblo (a stream de outro computador, um pet visitante) |
| "sem um bloco de memória inteiro: N KB livres" | room | Há bytes livres, mas não num bloco de need + 4 KB: fragmentação | Igual ao anterior; maxBlock em /api/info mostra o bloco |
| "o arquivo ou o player falhou" | failed | O arquivo no slot não abre ou não valida | Mande a cena de novo (screen.animate); o SDK regrava o slot |
| "(memória emprestada a uma requisição)" | yielded sem contagens | A cena estava carregada e cedeu lugar a uma requisição; volta em 150 ms | Nada |
| "memória emprestada N×" | yields | A cena toca, mas cede lugar com frequência | Normal em poucas vezes por minuto; a cada segundo, é a ponte mandando requisições grandes demais (cards grandes, snapshots a cada segundo) |
Uma cena que fica em free ou room com a tela App no vidro é a conta desta página: a sua necessidade (anim.need) + 8 KB não cabe no idleHeap. Os números de anim.compose dizem se os itens estão sendo compostos (ok crescendo) ou redesenhados do jeito antigo (noLayer crescendo): no segundo caso o heap está no limite durante o desenho, mesmo com a cena carregada.