dev/render-all-scene-cache-and-camera-integration #16

Merged
bquarkz merged 21 commits from dev/render-all-scene-cache-and-camera-integration into master 2026-04-18 16:20:50 +00:00
8 changed files with 1147 additions and 1 deletions
Showing only changes of commit d5d63d79c0 - Show all commits

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":26,"AGD":26,"DEC":14,"PLN":17,"LSN":31,"CLSN":1}}
{"type":"meta","next_id":{"DSC":27,"AGD":27,"DEC":15,"PLN":22,"LSN":31,"CLSN":1}}
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"workflow/agendas/AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"workflow/decisions/DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"workflow/plans/PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
@ -18,6 +18,7 @@
{"type":"discussion","id":"DSC-0013","status":"done","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"lessons/DSC-0013-perf-host-debug-overlay-isolation/LSN-0027-host-debug-overlay-isolation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0024","status":"done","ticket":"generic-memory-bank-slot-contract","title":"Agenda - Generic Memory Bank Slot Contract","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["runtime","asset","memory-bank","slots","host"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"lessons/DSC-0024-generic-memory-bank-slot-contract/LSN-0029-slot-first-bank-telemetry-belongs-in-asset-manager.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0025","status":"done","ticket":"scene-bank-and-viewport-cache-refactor","title":"Scene Bank and Viewport Cache Refactor","created_at":"2026-04-11","updated_at":"2026-04-14","tags":["gfx","tilemap","runtime","render"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"lessons/DSC-0025-scene-bank-and-viewport-cache-refactor/LSN-0030-canonical-scene-cache-and-resolver-split.md","status":"done","created_at":"2026-04-14","updated_at":"2026-04-14"}]}
{"type":"discussion","id":"DSC-0026","status":"open","ticket":"render-all-scene-cache-and-camera-integration","title":"Integrate render_all with Scene Cache and Camera","created_at":"2026-04-14","updated_at":"2026-04-14","tags":["gfx","runtime","render","camera","scene"],"agendas":[{"id":"AGD-0026","file":"workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14"}],"decisions":[{"id":"DEC-0014","file":"workflow/decisions/DEC-0014-frame-composer-render-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_agenda":"AGD-0026"}],"plans":[{"id":"PLN-0017","file":"workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]},{"id":"PLN-0018","file":"workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]},{"id":"PLN-0019","file":"workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]},{"id":"PLN-0020","file":"workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]},{"id":"PLN-0021","file":"workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}

View File

@ -0,0 +1,346 @@
---
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.

View File

@ -0,0 +1,186 @@
---
id: DEC-0014
ticket: render-all-scene-cache-and-camera-integration
title: Frame Composer Render Integration
status: accepted
created: 2026-04-14
accepted: 2026-04-14
agenda: AGD-0026
plans: [PLN-0017, PLN-0018, PLN-0019, PLN-0020, PLN-0021]
tags: [gfx, runtime, render, camera, scene, sprites]
---
## Status
Accepted.
## Contexto
`DSC-0025` closed the canonical scene model around `SceneBank`, `SceneViewportCache`, and `SceneViewportResolver`, but the operational frame loop still remained split. `Gfx` still exposed `render_all()`, while the new world path already existed separately as `render_scene_from_cache(...)`.
This left the runtime with an incomplete composition model:
- canonical scene/camera/cache architecture had already changed;
- the normal frame entrypoint had not yet been integrated with that architecture;
- sprite ownership was still too coupled to `Gfx` and to a slot-first `active` model.
This decision closes the ownership and composition model for the next integration phase.
## Decisao
The runtime SHALL converge to a `FrameComposer`-owned frame orchestration model.
Normatively:
- `Gfx.render_all()` MUST be retired as the canonical frame service.
- The canonical operational frame entrypoint SHALL become `FrameComposer.render_frame()`.
- `FrameComposer` SHALL live in `hardware/drivers`, alongside `Gfx`.
- `Hardware` SHALL aggregate both `FrameComposer` and `Gfx`.
- `FrameComposer` SHALL own the frame-operational state:
- active scene binding;
- camera / viewport state;
- `SceneViewportCache`;
- `SceneViewportResolver`;
- sprite submission state through `SpriteController`.
- `Gfx` SHALL remain a low-level visual backend responsible for composition, blit, and raster execution.
- `Gfx` MUST NOT remain the owner of scene state or sprite submission state.
## Rationale
This split preserves a clean ownership model:
- `FrameComposer` decides what the frame is;
- `Gfx` executes how the frame is drawn.
Keeping orchestration in `FrameComposer` avoids re-entangling renderer code with camera policy, cache refresh policy, and scene binding. Keeping `FrameComposer` in `hardware/drivers` instead of `hal` preserves room for backend-specific acceleration while avoiding a policy-heavy abstraction in HAL.
This also preserves future backend freedom:
- software path today;
- hardware-assisted blit path later;
- or a more PPU-like backend in bare-metal environments.
## Invariantes / Contrato
### 1. Frame Entry
- The canonical public frame orchestration path SHALL be `FrameComposer.render_frame()`.
- `FrameComposer.render_frame()` SHALL be capable of producing a valid frame 100% of the time.
- A valid frame MUST NOT require a scene to be bound.
### 2. No-Scene Behavior
- If no scene is bound, `FrameComposer.render_frame()` SHALL compose only:
- emitted sprites;
- fades already owned by the visual backend.
- No implicit clear SHALL be performed.
- Clearing the back buffer SHALL remain the responsibility of the caller / developer.
- In the no-scene state:
- cache MUST be absent or inert;
- resolver MUST be absent or inert;
- the system SHALL expose explicit scene-availability status.
### 3. Scene Binding
- Scene binding SHALL be performed by `bind_scene(scene_bank_id)`.
- `FrameComposer` SHALL depend on `SceneBankPoolAccess`.
- `FrameComposer` MUST resolve scenes through the pool, not through copied scene values.
- Scene access MUST be pointer-based / shared-reference based only.
- On bind, `FrameComposer` SHALL store:
- `scene_bank_id`;
- `Arc<SceneBank>` for the resolved scene.
- The `SceneViewportCache` SHALL live inside `FrameComposer` while the scene remains bound.
- `unbind_scene()` SHALL:
- remove the active scene;
- discard the associated cache;
- invalidate the world path;
- keep the frame path valid for no-scene composition.
- Replacing the contents of a bound scene slot SHALL require a new explicit bind.
- `FrameComposer` MUST NOT poll the scene bank pool each frame to revalidate the binding.
- A new `bind_scene(...)` SHALL replace the previous bound scene completely.
### 4. Camera
- The V1 camera contract SHALL be minimal.
- `set_camera(x, y)` SHALL accept `i32` pixel coordinates.
- `x` and `y` SHALL represent the top-left of the viewport in world space.
- Camera follow, smoothing, shake, cinematic transitions, and similar behaviors are OUT OF SCOPE for this decision.
### 5. Cache and Resolver
- `FrameComposer` SHALL own both `SceneViewportCache` and `SceneViewportResolver`.
- `FrameComposer` SHALL apply `CacheRefreshRequest`s to the cache.
- `Gfx` MUST NOT own cache refresh policy.
- `Gfx` MUST only consume already prepared render state.
### 6. Sprite Model
- `Sprite.active` MUST be removed from the canonical operational model.
- Sprite submission SHALL become frame-emission based.
- `SpriteController` SHALL be the sprite submission subsystem owned by `FrameComposer`.
- The sprite frame capacity SHALL remain capped at `512` for V1.
- The sprite counter SHALL be reset at the start of each frame.
- The caller MUST NOT provide sprite indices directly.
- Each `emit_sprite(...)` call SHALL occupy the next available internal slot.
- Overflow beyond capacity SHALL be ignored.
- Overflow SHOULD leave room for system logging / telemetry.
- Future certification MAY penalize sprite overflow, but that is not part of this decision.
- `emit_sprite(...)` SHALL NOT require a dedicated reset API beyond the normal frame lifecycle.
### 7. Sprite Ordering
- Each sprite SHALL carry:
- `layer`;
- `priority`.
- `layer` SHALL remain numeric for now.
- The sprite `layer` type SHALL match the scene layer reference type used by the scene model.
- Composition SHALL be layer-based.
- Within a layer:
- lower `priority` SHALL render first;
- ties SHALL resolve FIFO by emission order.
### 8. Composition Scope
- HUD integration is OUT OF SCOPE for the first integration phase covered by this decision.
- The first integration phase SHALL focus on:
- world scene path;
- sprites;
- fades.
## Impactos
### HAL
- `GfxBridge` and adjacent visual contracts will need to stop treating `render_all()` as the canonical operational frame path.
### Drivers / Hardware
- `Hardware` will need to aggregate `FrameComposer` next to `Gfx`.
- `Gfx` will need to lose ownership of scene/sprite operational state.
- Sprite submission state will need to move into `SpriteController`.
### Runtime / VM
- The VM runtime will eventually trigger frame composition through the new `FrameComposer` path rather than depending on `Gfx.render_all()`.
- The VM/runtime side should not own the detailed cache or scene orchestration policy directly once `FrameComposer` exists in hardware/drivers.
### Asset / Scene Flow
- Scene activation will become explicit through bank-id binding.
- Scene slot replacement will require explicit rebinding behavior from callers.
## Referencias
- [AGD-0026-render-all-scene-cache-and-camera-integration.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md)
- [LSN-0030-canonical-scene-cache-and-resolver-split.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/lessons/DSC-0025-scene-bank-and-viewport-cache-refactor/LSN-0030-canonical-scene-cache-and-resolver-split.md)
## Propagacao Necessaria
- A new implementation plan MUST be created from this decision before code changes.
- `FrameComposer` and `SpriteController` need explicit planning and migration sequencing.
- `Gfx.render_all()` retirement MUST be planned rather than removed ad hoc.
- The frame service rename and integration path MUST be propagated through the frame loop callsites.
## Revision Log
- 2026-04-14: Initial accepted decision from `AGD-0026`.

View File

@ -0,0 +1,117 @@
---
id: PLN-0017
ticket: render-all-scene-cache-and-camera-integration
title: Plan - FrameComposer Core and Hardware Ownership
status: accepted
created: 2026-04-14
completed:
tags: [gfx, runtime, render, hardware, frame-composer]
---
## Objective
Introduce `FrameComposer` as a first-class hardware-side subsystem and move canonical frame orchestration ownership out of `Gfx`.
## Background
`DEC-0014` locks `FrameComposer` as the canonical frame orchestration service. The first implementation step is to create the owning type, place it in `hardware/drivers`, and make `Hardware` aggregate it next to `Gfx` without yet completing the full render-path migration.
## Scope
### Included
- Create the `FrameComposer` type in `crates/console/prometeu-drivers`.
- Define the minimal owned state shape:
- active scene binding state;
- camera / viewport state;
- optional cache;
- optional resolver;
- owned `SpriteController`.
- Aggregate `FrameComposer` inside `Hardware`.
- Expose the minimum driver-facing surface required for subsequent plans.
### Excluded
- full sprite-model migration
- full scene binding implementation
- cache refresh application
- render-path retirement of `Gfx.render_all()`
## Execution Steps
### Step 1 - Introduce the `FrameComposer` module and owned state
**What:**
Create `FrameComposer` as a concrete driver-side subsystem.
**How:**
- Add a new module such as `crates/console/prometeu-drivers/src/frame_composer.rs`.
- Define a `FrameComposer` struct with explicit placeholders for:
- `active_scene_id`
- `active_scene`
- `scene_status`
- `camera_x_px`
- `camera_y_px`
- `SceneViewportCache`
- `SceneViewportResolver`
- `SpriteController`
- Keep scene/cache/resolver fields optional where no-scene operation is required.
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
- `crates/console/prometeu-drivers/src/lib.rs`
### Step 2 - Aggregate `FrameComposer` in `Hardware`
**What:**
Make `Hardware` own `FrameComposer` alongside `Gfx`.
**How:**
- Extend `Hardware` with a `frame_composer` field.
- Wire construction so `FrameComposer` receives the shared bank access it needs for later plans.
- Keep ownership boundaries explicit: `FrameComposer` prepares frame state, `Gfx` remains backend.
**File(s):**
- `crates/console/prometeu-drivers/src/hardware.rs`
- `crates/console/prometeu-drivers/src/memory_banks.rs`
### Step 3 - Define the minimum public driver-facing surface
**What:**
Give the driver layer a stable initial surface for `FrameComposer`.
**How:**
- Expose minimal constructor and accessor paths.
- Do not yet overdesign HAL-facing traits.
- Ensure the code compiles with no implicit dependence on `Gfx.render_all()` ownership for frame state.
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
- `crates/console/prometeu-drivers/src/hardware.rs`
- `crates/console/prometeu-drivers/src/lib.rs`
## Test Requirements
### Unit Tests
- `FrameComposer` can be constructed without a bound scene.
- `Hardware` successfully constructs with both `gfx` and `frame_composer`.
### Integration Tests
- Shared bank access needed by `FrameComposer` is available through hardware construction.
### Manual Verification
- Inspect the resulting type ownership and confirm scene/sprite state is no longer being newly introduced into `Gfx`.
## Acceptance Criteria
- [ ] `FrameComposer` exists as a dedicated driver-side subsystem.
- [ ] `Hardware` aggregates `FrameComposer` next to `Gfx`.
- [ ] `FrameComposer` has explicit owned placeholders for scene/camera/cache/resolver/sprites.
- [ ] The build remains green with the new ownership structure in place.
## Dependencies
- Source decision: `DEC-0014`
## Risks
- Introducing `FrameComposer` with too much behavior too early can blur later migration steps.
- Introducing too little owned state can leave ownership ambiguous and force rework in later plans.

View File

@ -0,0 +1,127 @@
---
id: PLN-0018
ticket: render-all-scene-cache-and-camera-integration
title: Plan - SpriteController and Frame Emission Model
status: accepted
created: 2026-04-14
completed:
tags: [gfx, runtime, render, sprites, frame-composer]
---
## Objective
Replace the slot-first sprite model with a `FrameComposer`-owned `SpriteController` that emits sprites per frame instead of relying on `Sprite.active` and caller-provided indices.
## Background
`DEC-0014` removes `Sprite.active` from the canonical operational model and locks sprite submission to a frame-emission model owned by `SpriteController`.
## Scope
### Included
- Introduce `SpriteController`.
- Remove the operational dependence on `Sprite.active`.
- Remove caller-owned sprite indices from the canonical submission path.
- Add layer + priority ordering with FIFO tie-breaking.
- Preserve the capacity cap of `512` sprites per frame.
### Excluded
- HUD integration
- scene binding
- cache refresh logic
## Execution Steps
### Step 1 - Redefine the sprite operational model
**What:**
Move canonical sprite submission semantics from slot-first to frame-emission.
**How:**
- Update `Sprite` and adjacent APIs so the canonical path no longer depends on `active`.
- Keep layer numeric and aligned with the scene layer reference type.
- Preserve `priority` as the within-layer ordering field.
**File(s):**
- `crates/console/prometeu-hal/src/sprite.rs`
- any adjacent driver-side sprite helpers
### Step 2 - Implement `SpriteController`
**What:**
Create the owned sprite subsystem under `FrameComposer`.
**How:**
- Add a `SpriteController` type with:
- storage capacity `512`
- frame counter
- per-layer buckets
- stable FIFO semantics for equal priority
- Add `begin_frame()` behavior that clears counters and buckets.
- Add `emit_sprite(...)` behavior that appends to the next internal slot.
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
- optional dedicated `sprite_controller.rs`
### Step 3 - Handle overflow and logging semantics
**What:**
Implement overflow behavior without turning it into a hard runtime failure.
**How:**
- Ignore sprites emitted after capacity is reached.
- Leave explicit room for system logging / telemetry.
- Do not add a special reset API beyond the normal frame lifecycle.
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
- related telemetry/log hooks if needed
### Step 4 - Remove stale slot-first sprite entrypoints
**What:**
Retire the old “set sprite by explicit index” path from the canonical model.
**How:**
- Identify the current caller-facing `Gfx` sprite mutation surface.
- Migrate it toward `FrameComposer`-owned submission.
- Keep transitional shims only if required to preserve buildability for the next plan.
**File(s):**
- `crates/console/prometeu-drivers/src/gfx.rs`
- `crates/console/prometeu-hal/src/gfx_bridge.rs`
- `crates/console/prometeu-drivers/src/frame_composer.rs`
## Test Requirements
### Unit Tests
- `begin_frame()` resets sprite count and buckets.
- `emit_sprite(...)` appends without caller-provided index.
- lower `priority` renders first within a layer.
- equal `priority` resolves FIFO by registration order.
- overflow drops excess sprites without panicking.
### Integration Tests
- `FrameComposer` can emit sprites and provide ordered sprite state for rendering.
### Manual Verification
- Confirm no canonical submission path requires `Sprite.active` or explicit slot index anymore.
## Acceptance Criteria
- [ ] `SpriteController` exists under `FrameComposer`.
- [ ] `Sprite.active` is no longer required by the canonical frame path.
- [ ] Caller-provided sprite indices are retired from the canonical submission path.
- [ ] Layer/priority/FIFO ordering is implemented and tested.
- [ ] Overflow is ignored with space left for logging.
## Dependencies
- Depends on `PLN-0017`
- Source decision: `DEC-0014`
## Risks
- Keeping compatibility shims too long can leave the codebase in a dual sprite model.
- Removing index-based APIs too early may break callsites before `FrameComposer` integration is ready.

View File

@ -0,0 +1,127 @@
---
id: PLN-0019
ticket: render-all-scene-cache-and-camera-integration
title: Plan - Scene Binding, Camera, and Scene Status
status: accepted
created: 2026-04-14
completed:
tags: [gfx, runtime, render, scene, camera, frame-composer]
---
## Objective
Implement the `FrameComposer` scene-binding contract, minimal camera state, and explicit scene-availability status without yet completing the cache-refresh render path.
## Background
`DEC-0014` locks scene activation around `bind_scene(scene_bank_id)` with `SceneBankPoolAccess`, pointer-based access only, and `scene_bank_id + Arc<SceneBank>` retained inside `FrameComposer`.
## Scope
### Included
- scene bind/unbind contract
- active scene identity and shared reference storage
- scene availability status
- minimal camera state (`i32`, top-left viewport)
### Excluded
- applying cache refreshes
- full render-path migration
- HUD behavior
## Execution Steps
### Step 1 - Add scene binding state to `FrameComposer`
**What:**
Implement the canonical bind/unbind surface.
**How:**
- Add `bind_scene(scene_bank_id)` and `unbind_scene()`.
- Resolve scenes from `SceneBankPoolAccess`.
- Store both:
- `scene_bank_id`
- `Arc<SceneBank>`
- Replace prior scene binding completely on a new bind.
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
- `crates/console/prometeu-drivers/src/memory_banks.rs`
### Step 2 - Add explicit scene status
**What:**
Expose scene availability through status, not just implicit option checks.
**How:**
- Define a scene status enum or equivalent status object.
- Distinguish at least:
- no scene bound
- bound and available
- bound but not renderable if such intermediate state is needed
- Ensure no-scene rendering remains valid.
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
- optional HAL-facing status surface if needed later
### Step 3 - Add camera contract
**What:**
Implement the V1 camera ownership inside `FrameComposer`.
**How:**
- Add `set_camera(x, y)`.
- Store camera coordinates as `i32`.
- Treat them as top-left viewport coordinates in world space.
- Keep all advanced camera behavior out of scope.
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
### Step 4 - Tie cache/resolver lifetime to scene binding
**What:**
Align cache/resolver lifetime with the active scene contract.
**How:**
- Cache and resolver remain `None` / absent when no scene is bound.
- On bind:
- create or reinitialize cache/resolver.
- On unbind:
- discard cache/resolver and invalidate the world path.
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
## Test Requirements
### Unit Tests
- bind stores `scene_bank_id + Arc<SceneBank>`.
- unbind clears active scene and cache.
- scene status reflects no-scene and active-scene states.
- camera coordinates are stored as top-left pixel-space values.
### Integration Tests
- `FrameComposer` can resolve a scene from the pool and survive no-scene operation.
### Manual Verification
- Confirm scene access remains pointer-based and no scene copies are introduced.
## Acceptance Criteria
- [ ] `FrameComposer` binds scenes by bank id through `SceneBankPoolAccess`.
- [ ] Active binding stores both scene id and shared scene reference.
- [ ] Scene status is explicit.
- [ ] Camera contract is implemented as `i32` top-left viewport coordinates.
- [ ] Cache/resolver lifetime follows scene bind/unbind.
## Dependencies
- Depends on `PLN-0017`
- Source decision: `DEC-0014`
## Risks
- Weak scene-status semantics can make no-scene behavior ambiguous in later render integration.
- If cache/resolver lifetime is not tied cleanly to binding, stale world state can leak across scene transitions.

View File

@ -0,0 +1,124 @@
---
id: PLN-0020
ticket: render-all-scene-cache-and-camera-integration
title: Plan - Cache Refresh and render_frame Path
status: accepted
created: 2026-04-14
completed:
tags: [gfx, runtime, render, cache, resolver, frame-composer]
---
## Objective
Connect `FrameComposer` to `SceneViewportResolver`, apply cache refreshes inside `FrameComposer`, and establish `render_frame()` as the canonical composition path for world + sprites + fades.
## Background
`DEC-0014` requires that cache refresh policy remain inside `FrameComposer` and that `FrameComposer.render_frame()` become the canonical frame entry while `Gfx` remains only the low-level execution backend.
## Scope
### Included
- apply `CacheRefreshRequest`s in `FrameComposer`
- connect camera/scene state to resolver updates
- use cache-backed world rendering in the frame path
- keep valid no-scene rendering (`sprites + fades`)
### Excluded
- HUD integration
- final retirement cleanup of legacy callsites
## Execution Steps
### Step 1 - Apply resolver refreshes inside `FrameComposer`
**What:**
Move cache-refresh orchestration fully into `FrameComposer`.
**How:**
- On active-scene frames:
- call resolver update with current camera and scene
- consume returned `CacheRefreshRequest`s
- apply them to `SceneViewportCache`
- Keep `Gfx` unaware of refresh semantics.
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
### Step 2 - Define `render_frame()` as the canonical frame path
**What:**
Introduce the new frame service on `FrameComposer`.
**How:**
- Add `render_frame()` to `FrameComposer`.
- If a scene is active and renderable:
- prepare resolver update
- refresh cache
- call the cache-backed world path in `Gfx`
- If no scene is active:
- call the no-scene path for `sprites + fades`
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
- `crates/console/prometeu-drivers/src/gfx.rs`
### Step 3 - Keep `Gfx` as backend only
**What:**
Narrow `Gfx` to backend-oriented composition responsibilities.
**How:**
- Ensure `Gfx` consumes prepared state from `FrameComposer`.
- Do not let `Gfx` regain ownership of cache refresh or scene orchestration.
- Keep low-level helpers for cache-backed copy paths, sprite drawing, and fades in `Gfx`.
**File(s):**
- `crates/console/prometeu-drivers/src/gfx.rs`
### Step 4 - Cover scene and no-scene frame paths
**What:**
Protect the two canonical frame modes.
**How:**
- Add tests for:
- active-scene world composition
- no-scene `sprites + fades`
- scene transition through unbind/rebind
- cache refresh behavior staying inside `FrameComposer`
**File(s):**
- `crates/console/prometeu-drivers/src/frame_composer.rs`
- `crates/console/prometeu-drivers/src/gfx.rs`
## Test Requirements
### Unit Tests
- `render_frame()` with no scene produces valid no-scene composition.
- `render_frame()` with a scene applies resolver refreshes before composition.
- cache refresh requests are applied by `FrameComposer`, not `Gfx`.
### Integration Tests
- scene bind + camera set + sprite emission + `render_frame()` produces the expected composed frame.
### Manual Verification
- Verify that no-scene frames still render sprites/fades without crashes or hidden clears.
## Acceptance Criteria
- [ ] `FrameComposer.render_frame()` exists and is the canonical frame path.
- [ ] Cache refreshes are applied inside `FrameComposer`.
- [ ] World rendering consumes the cache-backed path.
- [ ] No-scene `sprites + fades` behavior remains valid.
- [ ] `Gfx` remains backend-only for this path.
## Dependencies
- Depends on `PLN-0017`, `PLN-0018`, and `PLN-0019`
- Source decision: `DEC-0014`
## Risks
- If refresh application leaks into `Gfx`, the ownership split from `DEC-0014` collapses.
- If no-scene behavior is not tested explicitly, scene integration can accidentally make scene binding mandatory.

View File

@ -0,0 +1,118 @@
---
id: PLN-0021
ticket: render-all-scene-cache-and-camera-integration
title: Plan - Service Retirement, Callsite Migration, and Regression Coverage
status: accepted
created: 2026-04-14
completed:
tags: [gfx, runtime, render, migration, regression]
---
## Objective
Retire `Gfx.render_all()` from the canonical flow, migrate callsites to `FrameComposer.render_frame()`, and add the regression coverage needed to lock the new service model.
## Background
`DEC-0014` is explicit that `Gfx.render_all()` must be retired and that `FrameComposer.render_frame()` becomes the canonical frame orchestration entrypoint. This final plan removes the old canonical service shape and validates the migration end-to-end.
## Scope
### Included
- retire `Gfx.render_all()` from the canonical path
- migrate frame-loop callsites
- align bridge surfaces as needed
- add regression coverage for the final service model
### Excluded
- HUD integration
- future certification behavior for sprite overflow
## Execution Steps
### Step 1 - Migrate frame-loop callsites
**What:**
Switch runtime frame execution from `Gfx.render_all()` to `FrameComposer.render_frame()`.
**How:**
- Identify all canonical callsites that currently trigger `Gfx.render_all()`.
- Update them to go through `FrameComposer`.
- Preserve present/swap behavior after the render call.
**File(s):**
- `crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs`
- any additional runtime frame-loop callsites
### Step 2 - Retire `Gfx.render_all()` from the canonical service surface
**What:**
Remove the old frame service as the operational entry.
**How:**
- Remove or deprecate `render_all()` from `Gfx` and `GfxBridge` as the canonical render entry.
- Keep only backend-oriented helpers that `FrameComposer` calls.
- Ensure the naming and public path converge to Rust-style `render_frame()`.
**File(s):**
- `crates/console/prometeu-hal/src/gfx_bridge.rs`
- `crates/console/prometeu-drivers/src/gfx.rs`
### Step 3 - Add end-to-end regression coverage
**What:**
Protect the new service model against fallback to the old renderer path.
**How:**
- Add tests that prove:
- frame-loop code calls `FrameComposer.render_frame()`
- no-scene frames remain valid
- active-scene frames render through cache-backed composition
- sprite emission and ordering survive the full path
- Add assertions or test failures for accidental continued reliance on `Gfx.render_all()`.
**File(s):**
- runtime tests
- driver tests
- bridge tests where needed
### Step 4 - Validate full repository behavior
**What:**
Confirm the migration did not break unrelated systems.
**How:**
- Run the repository validation command required by current practice.
- Keep regression evidence attached to the plan execution.
**File(s):**
- repository-wide CI / validation entrypoints
## Test Requirements
### Unit Tests
- `Gfx` no longer exposes `render_all()` as the canonical operational frame path.
### Integration Tests
- runtime tick path renders through `FrameComposer.render_frame()`.
- no-scene and active-scene frame modes both remain valid.
### Manual Verification
- Run the repository CI path and confirm the final integrated service model is green.
## Acceptance Criteria
- [ ] Frame-loop callsites use `FrameComposer.render_frame()`.
- [ ] `Gfx.render_all()` is retired from the canonical service path.
- [ ] Regression coverage protects against fallback to the old model.
- [ ] Repository validation passes after the migration.
## Dependencies
- Depends on `PLN-0017`, `PLN-0018`, `PLN-0019`, and `PLN-0020`
- Source decision: `DEC-0014`
## Risks
- Removing `render_all()` too early can strand intermediate callsites.
- Leaving it in place as a canonical path for too long can create a dual-service model that is harder to remove later.