prometeu-runtime/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md
bQUARKz 98d2d81882
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
adjustments over frame composer contract - agnostic tile size
2026-04-15 08:42:46 +01:00

17 KiB

id ticket title status created updated tags
AGD-0026 render-all-scene-cache-and-camera-integration Agenda - Integrate render_all with Scene Cache and Camera accepted 2026-04-14 2026-04-15
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, 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
  • O FrameComposer não pode regredir o contrato já aceito no scene model:
    • SceneLayer.tile_size já é por-layer e aceita 8x8, 16x16 e 32x32;
    • o decoder de SCENE já materializa esses tamanhos;
    • fixar o pipeline em 16x16 dentro do orquestrador de frame criaria uma restrição artificial que não existe no modelo canônico.
  • 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 CacheRefreshRequests 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.

Reabertura 2026-04-15

Contexto adicional

Ao revisitar a thread, apareceu uma restrição indevida: tratar o FrameComposer como se aceitasse apenas tilesets 16x16.

Isso conflita com o estado atual do runtime:

  • TileSize no HAL já enumera Size8, Size16 e Size32;
  • SceneLayer carrega tile_size por layer;
  • o decoder de SCENE já aceita 8, 16 e 32;
  • SceneViewportResolver e SceneViewportCache já calculam offsets, anchors e cópia a partir do tile_size da própria layer.

O risco aqui não é apenas de implementação. Se o contrato do FrameComposer assumir 16x16 como pré-condição, ele quebra a neutralidade do orquestrador e reabre uma limitação artificial acima do scene model.

Problema reaberto

Precisamos fechar explicitamente que o FrameComposer aceita cenas/layers com tile_size 8x8 e não impõe 16x16 como tamanho mínimo ou obrigatório para o world path.

Opcoes adicionais

Opcao 4 - Fixar 16x16 no FrameComposer e tratar 8x8 como fora de escopo

Vantagens:

  • reduz casos de teste imediatos;
  • simplifica implementação inicial se alguém estiver assumindo viewport/caches calibrados manualmente para 16.

Desvantagens:

  • contradiz o scene model já aceito;
  • introduz restrição artificial no orquestrador;
  • obriga futura revisão de contrato para reaceitar algo que a base já suporta.

Opcao 5 - Manter FrameComposer tile-size agnostic e aceitar 8x8 desde V1

Vantagens:

  • preserva o contrato canônico já existente em SceneLayer;
  • mantém o FrameComposer como orquestrador, não como redefinidor de formato;
  • evita bifurcação entre pipeline de scene e pipeline de composição.

Desvantagens:

  • exige deixar isso explícito na decisão e nos planos;
  • aumenta a exigência de testes para viewport/cache/cópia com 8x8.

Recomendacao adicional

Seguir com a Opcao 5.

Norma proposta para fechamento desta reabertura:

  • FrameComposer deve aceitar scenes/layers cujo tile_size resolvido seja 8x8, 16x16 ou 32x32;
  • FrameComposer nao deve impor 16x16 como pré-condição para bind, cache, resolver ou composição;
  • qualquer validação de compatibilidade deve ser derivada do tile_size declarado pela própria layer / glyph bank, nunca de um default rígido no compositor;
  • os planos derivados desta thread precisam citar testes explícitos para 8x8.

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;
  • que o FrameComposer permanece agnóstico ao tile_size canônico da layer e aceita 8x8 sem downgrade contratual;
  • 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.