Pular para o conteúdo
miblo

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

Do app aos pixels finaisApp + SDKdados, arte e movimentoPonte no computadorvalidação e planejamentoCena residentetiles e comandos compactosCardtexto e componentesDesenho por linhafundo, sprites e efeitosComposição → SPI → telapixels finais da região
O computador prepara a cena. O aparelho combina movimento e conteúdo antes de enviar os pixels da região ao painel.

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úmeroO que é
12 KBA 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 KBO 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 KBA 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

O cache se adapta às cores de cada tileTile 8 × 8 · 64 pixels1 corconstante · 1 B2 cores1 bit/pixel · 9 B3–4 cores2 bits/pixel · 18 B5–16 cores4 bits/pixel · 32 BDiretório + dados: usar somente se o total for menor
O diretório custa 2 bytes por tile. O cache compacto só é escolhido se o total economizar memória, com todos os pixels preservados.

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:

ParteBytes
Os tiles residenteso tamanho compactado do conjunto (abaixo)
O player3.104
O cache de paletas528

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 tileComo ficaBytes
1uma constante1
2dois índices e 1 bit por pixel9
3 ou 4quatro índices e 2 bits por pixel18
5 a 16os 4 bits por pixel do arquivo32

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

Texto e animação no mesmo envioDesenho separadoFundo → paineltexto apagado por um instanteLetras → paineltexto reapareceComposição por faixaFundo + letras em RAMfaixa de 240 × 16 pixelsPixels finais → paineltexto já sobre a animação
Cada faixa é composta antes de ser enviada. O painel deixa de receber primeiro um fundo sem as letras naquela regiã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:

  1. As capturas dos itens vão primeiro (o orçamento delas vai a zero).

  2. 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

CampoO que é
heapO heap livre no instante da resposta, dentro do handler, depois de ele alocar o próprio documento (~2 KB). Não compare com idleHeap
idleHeapO heap livre amostrado pelo laço principal, fora de qualquer requisição. É o número para comparar entre leituras e entre versões
maxBlockO maior bloco inteiro livre. Muito menor que idleHeap é fragmentação
minHeapParseO menor heap livre visto durante a leitura de um snapshot
petDrawingBytes, guestDrawingBytesOs buffers de desenho do seu pet e do pet visitante, se estão carregados (0 quando devolvidos)
petLogoBytesO logo do pet personalizado, que fica
snapshotTextBytes, snapshotTextUsedO pool dos textos das sessões: o alocado e o usado

GET /api/screen

CampoO que é
panelO 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.loadedA cena está carregada na RAM
anim.yieldedNão está porque a memória dela foi emprestada; ela volta sozinha
anim.needO que carregar custa: tiles compactados + player + cache (quando não está carregada)
anim.whyEnquanto 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.yieldNeedQuantas 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.floorYieldsQuantas vezes foi devolvida pelo piso de 8 KB
anim.composeok: 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.tileRawBytesCom 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
governorCada 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 dizanim.whyO que está acontecendoO que fazer
"a tela do app não está no Miblo"notShownA 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 vidroNada: ela carrega na vez do app. Confira panel
"uma requisição em curso"pendingA ponte está mandando algo (um card, um lote ao vivo) a cada passagemNormal por instantes; constante, é uma ponte mandando demais (um card a cada segundo, por exemplo)
"devolvendo a memória emprestada"holdA espera de 150 ms ou 2 s depois de um empréstimoNada
"sem memória livre: N KB livres"freeFaltou need + 8 KB. lastFree diz quanto haviaUma 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"roomHá bytes livres, mas não num bloco de need + 4 KB: fragmentaçãoIgual ao anterior; maxBlock em /api/info mostra o bloco
"o arquivo ou o player falhou"failedO arquivo no slot não abre ou não validaMande a cena de novo (screen.animate); o SDK regrava o slot
"(memória emprestada a uma requisição)"yielded sem contagensA cena estava carregada e cedeu lugar a uma requisição; volta em 150 msNada
"memória emprestada N×"yieldsA cena toca, mas cede lugar com frequênciaNormal 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.