prometeu-runtime/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md

347 lines
14 KiB
Markdown

---
id: AGD-0026
ticket: render-all-scene-cache-and-camera-integration
title: Agenda - Integrate render_all with Scene Cache and Camera
status: accepted
created: 2026-04-14
updated: 2026-04-14
tags: [gfx, runtime, render, camera, scene]
---
## Contexto
A thread `DSC-0025` fechou a base arquitetural para `SceneBank`, `SceneViewportCache`, `SceneViewportResolver` e o decoder binário de `SCENE`. O renderer já possui um caminho explícito `render_scene_from_cache(&SceneViewportCache, &ResolverUpdate)`, mas o loop operacional do runtime ainda chama apenas `render_all()`.
Hoje, em [tick.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs:148), o frame segue pelo `hw.gfx_mut().render_all()`. Isso significa que o caminho novo de world render ainda não está integrado ao ciclo normal do runtime.
Ao mesmo tempo, a integração correta depende de fechar o contrato mínimo da câmera, porque o `SceneViewportResolver` já assume uma posição de câmera em pixel space e produz `ResolverUpdate` a partir dela. Sem essa integração, o runtime fica com duas verdades práticas:
- a arquitetura aceita para world rendering;
- o caminho ainda efetivamente usado pelo frame loop.
## Problema
Precisamos integrar `render_scene_from_cache()` ao `render_all()` e ao ciclo real do runtime sem reabrir a arquitetura já aceita para `SceneBank` / `SceneViewportCache` / `SceneViewportResolver`.
O problema concreto não é só “chamar uma função”. É decidir:
- quem é dono do estado de câmera mínimo;
- onde `SceneBank`, `SceneViewportCache` e `SceneViewportResolver` passam a residir em runtime;
- quando o cache é atualizado;
- como o `render_all()` deixa de ser um caminho “scene-blind” e vira o entrypoint normal da composição final.
## Pontos Criticos
- `render_all()` deve continuar funcional mesmo quando nenhuma scene estiver carregada.
- O `render_all()` atual não desenha world layers; ele só compõe sprites de prioridade 0 e fades.
- O modelo atual de `Sprite.priority` mistura duas responsabilidades:
- em qual faixa de composição o sprite entra;
- qual a ordem relativa entre sprites naquela faixa.
- `render_scene_from_cache()` existe, mas exige `SceneViewportCache` e `ResolverUpdate` já preparados por fora.
- O modelo atual de sprites ainda é slot-first para o chamador:
- há armazenamento fixo;
- o dev informa índice;
- e o renderer precisa filtrar `active`.
- O `SceneViewportResolver` já carrega política importante:
- câmera em pixel space
- anchors
- clamp
- histerese
- refresh requests
- copy requests
- Ainda não existe um dono explícito do estado operacional:
- cena ativa
- cache ativo
- resolver ativo
- câmera ativa
- Se a integração for mal feita, o renderer pode voltar a misturar:
- política de câmera
- atualização de cache
- composição final
## Opcoes
### Opcao 1 - Integrar tudo diretamente dentro de `Gfx`
**Como seria:**
`Gfx` passa a possuir a scene ativa, o cache, o resolver e a câmera; `render_all()` atualiza resolver/cache e já compõe tudo.
**Vantagens:**
- caminho curto de integração;
- menos objetos atravessando o runtime;
- fácil de chamar a partir do tick.
**Desvantagens:**
- empurra para `Gfx` responsabilidade demais;
- mistura composição com estado de cena/câmera;
- reduz clareza para testes e evolução futura.
### Opcao 2 - Integrar no runtime com um controlador explícito de scene viewport
**Como seria:**
O runtime ou um pequeno controlador operacional passa a possuir:
- scene ativa
- cache
- resolver
- câmera
Esse controlador atualiza o cache quando a câmera muda e entrega ao `Gfx` apenas o que ele precisa para compor.
**Vantagens:**
- separa melhor estado operacional de composição;
- mantém `Gfx` mais focado em render;
- preserva a ideia de que o resolver é dono da política de movimento/rematerialização.
- permite manter um caminho explícito de `render_all()` sem scene carregada.
**Desvantagens:**
- adiciona mais um objeto operacional no runtime;
- exige definir uma superfície clara entre runtime e renderer.
### Opcao 3 - Fazer uma integração mínima temporária em `render_all()` e postergar a arquitetura do dono da câmera
**Como seria:**
Criar um caminho temporário para que `render_all()` receba ou consulte estado suficiente para chamar `render_scene_from_cache()`, mas sem ainda fechar onde mora a câmera a longo prazo.
**Vantagens:**
- acelera a ligação do caminho novo ao frame loop;
- destrava testes end-to-end rapidamente.
**Desvantagens:**
- alto risco de solução transitória virar definitiva;
- deixa ambiguidade operacional exatamente no ponto mais sensível da integração.
## Sugestao / Recomendacao
Seguir com a **Opcao 2**.
Ou seja:
- `FrameComposer` passa a ser o orquestrador de frame/scene;
- `FrameComposer` deve morar em `hardware/drivers`;
- `Hardware` passa a agregar `FrameComposer` ao lado de `Gfx`;
- `Gfx` permanece como backend de composição e blit;
- a política do frame não deve ficar presa ao hardware atual;
- isso preserva espaço para:
- fast paths com diretivas/capacidades de GPU quando existirem;
- uma futura implementação mais próxima de PPU / bare metal;
- `render_all()` deve continuar sendo o entrypoint normal de composição;
- `render_all()` deve continuar funcionando mesmo sem scene ativa;
- mas ele não deve virar dono da câmera nem do ciclo de atualização do cache;
- precisamos de um orquestrador operacional no runtime, ou imediatamente adjacente a ele, que:
- mantenha a scene ativa opcional;
- mantenha a câmera / viewport mínima;
- mantenha o controlador de sprites do frame;
- atualize o `SceneViewportResolver` quando houver scene;
- aplique refreshes ao `SceneViewportCache` quando houver scene;
- e entregue ao `Gfx` o estado pronto para compor.
Mais explicitamente:
- `FrameComposer` passa a ser dono de:
- scene ativa;
- câmera / viewport;
- `SceneViewportCache`;
- `SceneViewportResolver`;
- sprites emitidos no frame;
- o state de scene/sprite que hoje esteja em `Gfx` deve migrar para `FrameComposer`;
- `Gfx` deve ficar focado em:
- composição;
- blit;
- raster;
- execução visual do frame preparado.
Para V1, o contrato mínimo de câmera pode continuar pequeno:
- `camera_x_px: i32`
- `camera_y_px: i32`
- representando o canto superior esquerdo da viewport no mundo
Sem follow/smoothing/shake/cut nesta etapa.
O comportamento mínimo recomendado fica:
- sem scene ativa:
- `FrameComposer` continua válido;
- `render_all()` compõe apenas o que já existe fora do pipeline de world (`sprites`, `fades`, e futuramente `HUD` quando aplicável);
- não existe `clear` implícito;
- limpar o `back` continua sendo responsabilidade explícita do chamador / dev;
- com scene ativa:
- `FrameComposer` atualiza resolver/cache;
- `render_all()` compõe o world a partir do cache e preserva a ordem já aceita.
Esta direção é provisoriamente aceita mesmo sem a figura final completa, justamente para permitir que a integração avance e revele os pontos onde a separação runtime/backend ainda precise de ajuste.
Para sprites, a direção provisória recomendada fica:
- cada `Sprite` deve carregar:
- `layer`
- `priority`
- `Sprite.active` deve ser removido;
- `layer` define em qual faixa de composição o sprite entra;
- `priority` define a ordenação entre sprites daquela mesma faixa;
- a composição observável passa a ser por camada:
- `(sprites -> scene) layer_0`
- `(sprites -> scene) layer_1`
- `(sprites -> scene) layer_2`
- `(sprites -> scene) layer_3`
Isso substitui o modelo atual em que um único `priority` tenta representar ao mesmo tempo posição macro na composição e ordenação fina.
O modelo operacional recomendado para sprites passa a ser:
- capacidade máxima interna de `512` sprites por frame;
- contador zerado a cada frame;
- o dev não informa mais índice de sprite;
- cada emissão ocupa o próximo slot interno disponível;
- o registro já coloca o sprite no bucket correto da layer;
- a composição consome apenas os sprites emitidos naquele frame.
## Perguntas em Aberto
- Fechado provisoriamente:
- `FrameComposer` em `hardware/drivers`;
- `Hardware` agrega `FrameComposer` e `Gfx`;
- `Gfx` atua como backend operacional de composição.
- O contrato mínimo do `FrameComposer` precisa ser fechado normativamente.
- Fechado:
- o subsistema interno de sprites se chama `SpriteController`.
- `Sprite.layer` deve ser um enum fechado (`Layer0..Layer3`) ou um tipo mais genérico?
- fechado provisoriamente:
- manter numérico;
- usar o mesmo tipo/referência de layer do `SceneBank`.
- A composição por camada deve ser:
- `sprites -> scene` dentro de cada layer, como direção inicial,
- ou `scene -> sprites` para alguma camada específica?
- A ordenação entre sprites de uma mesma layer será:
- fechado:
- `priority` menor blita primeiro;
- em empate, FIFO por ordem de registro.
- Overflow de sprite no frame:
- fechado: excedentes são ignorados;
- deve existir espaço para log/telemetria;
- futuramente isso pode virar sinal negativo para certificação.
- `emit_sprite(...)` precisa retornar algo, ou ter reset separado além de `begin_frame()`?
- fechado por enquanto:
- não;
- usar apenas log do sistema para overflow/eventos operacionais;
- não introduzir reset extra além do fluxo normal do frame.
- `render_all()` deve:
- continuar sem parâmetros e consultar estado já preparado,
- ou ganhar uma nova superfície interna para receber o scene state preparado?
- direção aceita:
- `FrameComposer` chama o entrypoint de composição do backend visual;
- `Gfx.render_all()` deve morrer;
- o serviço deve migrar para `FrameComposer.renderFrame()`.
## Contrato Minimo Proposto
Direção proposta para V1 do `FrameComposer`:
- `bind_scene(...)`
- recebe um `scene bank id`;
- `FrameComposer` deve possuir acesso a `SceneBankPoolAccess`;
- resolve a scene ativa através do pool;
- o acesso ao bank deve ser sempre por ponteiro / referência compartilhada, nunca por cópia;
- ao bindar, o compositor guarda:
- `scene_bank_id`;
- `Arc<SceneBank>` já resolvido;
- consegue verificar se a scene está carregada;
- inicializa ou reinicializa cache/resolver conforme necessário.
- `unbind_scene()`
- remove a scene ativa;
- invalida o pipeline de world;
- descarta o cache associado à scene bindada;
- mantém o compositor funcional para `sprites + fades`.
- `set_camera(x, y)`
- atualiza a posição da câmera em pixel space;
- `x, y` representam o canto superior esquerdo da viewport no mundo.
- `begin_frame()`
- zera o contador de sprites emitidos;
- limpa buckets internos de sprite;
- prepara o estado transitório do frame.
- `emit_sprite(...)`
- registra um sprite no próximo slot interno disponível;
- associa o sprite à sua `layer`;
- insere no bucket correspondente;
- overflow é ignorado com espaço para log/telemetria.
- `compose_frame()`
- se houver scene ativa:
- atualiza `SceneViewportResolver`;
- aplica `CacheRefreshRequest`s ao `SceneViewportCache`;
- aciona o caminho de composição world + sprites;
- se não houver scene ativa:
- aciona o caminho `sprites + fades`;
- delega a composição efetiva ao `Gfx`.
### Observacoes
- `end_frame()` não parece obrigatório na V1.
- `begin_frame()` + `compose_frame()` já cobrem o ciclo mínimo.
- `FrameComposer` decide e prepara;
`Gfx` executa a composição.
- o binding de scene deve ser por `scene bank id`, não por ownership direto de `SceneBank`.
- o `SceneViewportCache` vive dentro do `FrameComposer` enquanto a scene estiver bindada.
- troca do conteúdo do slot/bank exige novo `bind_scene(...)`;
o `FrameComposer` não deve ficar fazendo polling constante do pool para revalidar a scene ativa.
- o fluxo operacional aceito é:
- `FrameComposer.compose_frame()`
- chama o serviço `FrameComposer.renderFrame()`.
- `FrameComposer` deve ser capaz de renderizar algo 100% do tempo:
- cache/resolver ficam `None` sem bind;
- deve existir uma forma explícita de saber se a scene está disponível para render.
- `bind_scene(...)` substitui completamente a scene anterior.
## Sugestao / Recomendacao Atualizada
Aceitar o contrato mínimo acima como base de fechamento da agenda, a menos que apareça alguma necessidade concreta de:
- separar `compose_frame()` em múltiplas fases públicas;
- expor refresh manual de cache para o chamador;
- ou introduzir um `end_frame()` com semântica real além do reset que já ocorre em `begin_frame()`.
- manter o binding de scene como:
- `scene_bank_id + Arc<SceneBank>`;
- com rebind explícito quando o slot mudar.
- Quem é responsável por aplicar `CacheRefreshRequest` ao `SceneViewportCache`:
- fechado: sempre o `FrameComposer`.
- Qual é o contrato explícito de “nenhuma scene carregada”:
- fechado: `sprites + fades`, sem `clear` implícito.
- Como a cena ativa é selecionada e trocada no ciclo real:
- fechado: `bind_scene(scene_bank_id)` com resolução através de `SceneBankPoolAccess`.
- O HUD entra nesta integração já agora, ou o foco da primeira integração é apenas world + sprites + fades?
- fechado: sem HUD nesta primeira integração.
## Criterio para Encerrar
Esta agenda pode ser encerrada quando estiver explícito:
- quem é dono do estado mínimo de câmera;
- quem é dono da scene/cache/resolver ativos;
- como funciona o bind/unbind da scene ativa;
- quando o cache é atualizado;
- como `render_all()` passa a compor o world path aceito;
- e qual é a superfície mínima de integração para implementação sem reabrir a arquitetura base.
## Resolucao
Esta agenda fica aceita com a seguinte direcao:
- `Gfx.render_all()` deve ser aposentado;
- o fluxo operacional deve convergir para `FrameComposer.render_frame()`;
- `FrameComposer` vive em `hardware/drivers`, ao lado de `Gfx`, e passa a ser dono do estado operacional do frame;
- `FrameComposer` deve manter:
- scene ativa opcional;
- camera/viewport;
- `SceneViewportCache`;
- `SceneViewportResolver`;
- `SpriteController`;
- scene ativa e acessada por `scene_bank_id + Arc<SceneBank>` via `SceneBankPoolAccess`, sem copias;
- troca de slot exige novo `bind_scene(...)`;
- sem scene ativa, o frame continua valido com `sprites + fades`, sem `clear` implicito;
- sprites passam a ser emitidos por frame, sem `Sprite.active`, com capacidade maxima de `512`, overflow ignorado e ordenacao por `layer`, `priority`, e FIFO em empate;
- HUD fica fora desta primeira integracao.