From d5d63d79c0e69d8c8d5d95cdd0de10a540b60f85 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 14 Apr 2026 07:28:00 +0100 Subject: [PATCH 01/21] Integrate render_all with Scene Cache and Camera --- discussion/index.ndjson | 3 +- ...-all-scene-cache-and-camera-integration.md | 346 ++++++++++++++++++ ...-0014-frame-composer-render-integration.md | 186 ++++++++++ ...me-composer-core-and-hardware-ownership.md | 117 ++++++ ...ite-controller-and-frame-emission-model.md | 127 +++++++ ...9-scene-binding-camera-and-scene-status.md | 127 +++++++ ...020-cache-refresh-and-render-frame-path.md | 124 +++++++ ...ement-callsite-migration-and-regression.md | 118 ++++++ 8 files changed, 1147 insertions(+), 1 deletion(-) create mode 100644 discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md create mode 100644 discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md create mode 100644 discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md create mode 100644 discussion/workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md create mode 100644 discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md create mode 100644 discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md create mode 100644 discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index e80e3372..8e8748d7 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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"}]} diff --git a/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md b/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md new file mode 100644 index 00000000..1dfa1409 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md @@ -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` 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`; + - 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` 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. diff --git a/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md b/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md new file mode 100644 index 00000000..ab824e2e --- /dev/null +++ b/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md @@ -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` 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`. diff --git a/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md b/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md new file mode 100644 index 00000000..80897371 --- /dev/null +++ b/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md @@ -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. diff --git a/discussion/workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md b/discussion/workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md new file mode 100644 index 00000000..c101524e --- /dev/null +++ b/discussion/workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md @@ -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. diff --git a/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md b/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md new file mode 100644 index 00000000..16991d27 --- /dev/null +++ b/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md @@ -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` 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` +- 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`. +- 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. diff --git a/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md b/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md new file mode 100644 index 00000000..240118f4 --- /dev/null +++ b/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md @@ -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. diff --git a/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md b/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md new file mode 100644 index 00000000..e95ecbae --- /dev/null +++ b/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md @@ -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. -- 2.47.2 From 98d2d81882bdfb3664e8cf6c65b931e472672f1a Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Wed, 15 Apr 2026 08:42:46 +0100 Subject: [PATCH 02/21] adjustments over frame composer contract - agnostic tile size --- discussion/index.ndjson | 2 +- ...-all-scene-cache-and-camera-integration.md | 61 ++++++++++++++++++- ...-0014-frame-composer-render-integration.md | 19 ++++++ ...me-composer-core-and-hardware-ownership.md | 3 + ...9-scene-binding-camera-and-scene-status.md | 4 ++ ...020-cache-refresh-and-render-frame-path.md | 8 +++ ...ement-callsite-migration-and-regression.md | 5 ++ 7 files changed, 100 insertions(+), 2 deletions(-) diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 8e8748d7..b8501bd6 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -18,7 +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-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-15","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-15"}],"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-15","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-15","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-15","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-15","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-15","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"}]} diff --git a/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md b/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md index 1dfa1409..756daaea 100644 --- a/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md +++ b/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md @@ -4,7 +4,7 @@ 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 +updated: 2026-04-15 tags: [gfx, runtime, render, camera, scene] --- @@ -54,6 +54,10 @@ O problema concreto não é só “chamar uma função”. É decidir: - 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 @@ -315,6 +319,60 @@ Aceitar o contrato mínimo acima como base de fechamento da agenda, a menos que - 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: @@ -324,6 +382,7 @@ Esta agenda pode ser encerrada quando estiver explícito: - 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 diff --git a/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md b/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md index ab824e2e..db95ec61 100644 --- a/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md +++ b/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md @@ -60,6 +60,12 @@ This also preserves future backend freedom: - hardware-assisted blit path later; - or a more PPU-like backend in bare-metal environments. +It also preserves the scene-model contract already accepted below the frame layer: + +- tile size is a property of each scene layer; +- the frame orchestrator must consume that contract, not redefine it; +- `FrameComposer` must therefore remain tile-size agnostic rather than hard-coding `16x16` assumptions. + ## Invariantes / Contrato ### 1. Frame Entry @@ -113,6 +119,15 @@ This also preserves future backend freedom: - `Gfx` MUST NOT own cache refresh policy. - `Gfx` MUST only consume already prepared render state. +### 5A. Tile Size Contract + +- `FrameComposer` SHALL remain tile-size agnostic. +- `FrameComposer` MUST accept scene layers whose canonical `tile_size` is `8x8`, `16x16`, or `32x32`. +- `FrameComposer` MUST NOT impose `16x16` as a bind-time, cache-time, resolver-time, or render-time precondition. +- Cache sizing, resolver math, and world-copy preparation SHALL derive from the `tile_size` declared by each bound scene layer. +- Compatibility checks, when needed, MUST be derived from canonical scene-layer and glyph-bank metadata rather than from a hard-coded compositor default. +- Any implementation path that only works for `16x16` tiles is NON-COMPLIANT with this decision. + ### 6. Sprite Model - `Sprite.active` MUST be removed from the canonical operational model. @@ -158,6 +173,7 @@ This also preserves future backend freedom: - `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`. +- `FrameComposer`, cache, and resolver integration must preserve per-layer `tile_size` semantics, including `8x8`. ### Runtime / VM @@ -168,6 +184,7 @@ This also preserves future backend freedom: - Scene activation will become explicit through bank-id binding. - Scene slot replacement will require explicit rebinding behavior from callers. +- Scene-driven tile-size metadata must propagate unchanged into `FrameComposer` orchestration and backend copy preparation. ## Referencias @@ -180,7 +197,9 @@ This also preserves future backend freedom: - `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. +- Plan steps and tests that cover world composition MUST explicitly include `8x8` tile-size coverage. ## Revision Log - 2026-04-14: Initial accepted decision from `AGD-0026`. +- 2026-04-15: Revision accepted to make `FrameComposer` explicitly tile-size agnostic and to require `8x8` support alongside `16x16` and `32x32`. diff --git a/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md b/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md index 80897371..fac4e898 100644 --- a/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md +++ b/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md @@ -54,6 +54,7 @@ Create `FrameComposer` as a concrete driver-side subsystem. - `SceneViewportResolver` - `SpriteController` - Keep scene/cache/resolver fields optional where no-scene operation is required. +- Do not introduce any fixed `16x16` assumption into owned state; tile-size-sensitive behavior must derive from bound scene metadata. **File(s):** - `crates/console/prometeu-drivers/src/frame_composer.rs` @@ -93,6 +94,7 @@ Give the driver layer a stable initial surface for `FrameComposer`. ### Unit Tests - `FrameComposer` can be constructed without a bound scene. - `Hardware` successfully constructs with both `gfx` and `frame_composer`. +- Construction and owned state shape do not encode `16x16` as an implicit world-path invariant. ### Integration Tests - Shared bank access needed by `FrameComposer` is available through hardware construction. @@ -115,3 +117,4 @@ Give the driver layer a stable initial surface for `FrameComposer`. - 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. +- Encoding `16x16` into the initial owned-state shape would create a contract violation that later plans would have to unwind. diff --git a/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md b/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md index 16991d27..33c3693d 100644 --- a/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md +++ b/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md @@ -15,6 +15,7 @@ Implement the `FrameComposer` scene-binding contract, minimal camera state, and ## Background `DEC-0014` locks scene activation around `bind_scene(scene_bank_id)` with `SceneBankPoolAccess`, pointer-based access only, and `scene_bank_id + Arc` retained inside `FrameComposer`. +The same decision also requires `FrameComposer` to remain tile-size agnostic and to preserve canonical per-layer `tile_size`, including `8x8`. ## Scope @@ -90,6 +91,7 @@ Align cache/resolver lifetime with the active scene contract. - create or reinitialize cache/resolver. - On unbind: - discard cache/resolver and invalidate the world path. +- Any initialization must derive layer math from the bound scene tile sizes instead of assuming `16x16`. **File(s):** - `crates/console/prometeu-drivers/src/frame_composer.rs` @@ -101,6 +103,7 @@ Align cache/resolver lifetime with the active scene contract. - 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. +- bind/unbind remains valid for scenes whose layers use `8x8` tiles. ### Integration Tests - `FrameComposer` can resolve a scene from the pool and survive no-scene operation. @@ -115,6 +118,7 @@ Align cache/resolver lifetime with the active scene contract. - [ ] Scene status is explicit. - [ ] Camera contract is implemented as `i32` top-left viewport coordinates. - [ ] Cache/resolver lifetime follows scene bind/unbind. +- [ ] Scene bind/cache/resolver setup preserves canonical per-layer tile sizes, including `8x8`. ## Dependencies diff --git a/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md b/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md index 240118f4..9f1064f4 100644 --- a/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md +++ b/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md @@ -15,6 +15,7 @@ Connect `FrameComposer` to `SceneViewportResolver`, apply cache refreshes inside ## 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. +`DEC-0014` also requires the world path to remain tile-size agnostic, with explicit support for `8x8`, `16x16`, and `32x32` scene-layer tile sizes. ## Scope @@ -41,6 +42,7 @@ Move cache-refresh orchestration fully into `FrameComposer`. - consume returned `CacheRefreshRequest`s - apply them to `SceneViewportCache` - Keep `Gfx` unaware of refresh semantics. +- Ensure resolver and refresh math follow the bound layer `tile_size` values rather than any fixed `16x16` default. **File(s):** - `crates/console/prometeu-drivers/src/frame_composer.rs` @@ -58,6 +60,7 @@ Introduce the new frame service on `FrameComposer`. - call the cache-backed world path in `Gfx` - If no scene is active: - call the no-scene path for `sprites + fades` +- World rendering must remain valid when the active scene uses `8x8` tiles. **File(s):** - `crates/console/prometeu-drivers/src/frame_composer.rs` @@ -87,6 +90,7 @@ Protect the two canonical frame modes. - no-scene `sprites + fades` - scene transition through unbind/rebind - cache refresh behavior staying inside `FrameComposer` + - active-scene composition with `8x8` tile-size layers **File(s):** - `crates/console/prometeu-drivers/src/frame_composer.rs` @@ -98,9 +102,11 @@ Protect the two canonical frame modes. - `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`. +- `render_frame()` with an `8x8` scene uses resolver/cache math derived from layer tile size rather than a `16x16` assumption. ### Integration Tests - scene bind + camera set + sprite emission + `render_frame()` produces the expected composed frame. +- scene bind + camera set + `8x8` scene + `render_frame()` produces the expected composed frame. ### Manual Verification - Verify that no-scene frames still render sprites/fades without crashes or hidden clears. @@ -112,6 +118,7 @@ Protect the two canonical frame modes. - [ ] World rendering consumes the cache-backed path. - [ ] No-scene `sprites + fades` behavior remains valid. - [ ] `Gfx` remains backend-only for this path. +- [ ] The world path is explicitly covered for `8x8` scenes without `16x16`-specific assumptions. ## Dependencies @@ -122,3 +129,4 @@ Protect the two canonical frame modes. - 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. +- If tests cover only `16x16`, a latent compositor regression against canonical `8x8` scenes can ship unnoticed. diff --git a/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md b/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md index e95ecbae..00edec77 100644 --- a/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md +++ b/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md @@ -15,6 +15,7 @@ Retire `Gfx.render_all()` from the canonical flow, migrate callsites to `FrameCo ## 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. +The same decision also requires the new canonical path to preserve scene-layer tile sizes such as `8x8`, not just `16x16`. ## Scope @@ -68,6 +69,7 @@ Protect the new service model against fallback to the old renderer path. - frame-loop code calls `FrameComposer.render_frame()` - no-scene frames remain valid - active-scene frames render through cache-backed composition + - active-scene frames remain valid for canonical `8x8` scenes - sprite emission and ordering survive the full path - Add assertions or test failures for accidental continued reliance on `Gfx.render_all()`. @@ -96,6 +98,7 @@ Confirm the migration did not break unrelated systems. ### Integration Tests - runtime tick path renders through `FrameComposer.render_frame()`. - no-scene and active-scene frame modes both remain valid. +- runtime tick path remains valid when the active scene uses `8x8` tiles. ### Manual Verification - Run the repository CI path and confirm the final integrated service model is green. @@ -106,6 +109,7 @@ Confirm the migration did not break unrelated systems. - [ ] `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. +- [ ] Regression coverage includes the canonical `8x8` world-path case. ## Dependencies @@ -116,3 +120,4 @@ Confirm the migration did not break unrelated systems. - 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. +- Migrating callsites without `8x8` regression coverage can falsely validate only the legacy `16x16` path. -- 2.47.2 From 94c80e61baac782bcfd809f3788769135636844d Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 13:09:27 +0100 Subject: [PATCH 03/21] adjustments over frame composer contract - agnostic tile size --- .../PLN-0017-frame-composer-core-and-hardware-ownership.md | 1 + .../plans/PLN-0019-scene-binding-camera-and-scene-status.md | 4 ++++ .../plans/PLN-0020-cache-refresh-and-render-frame-path.md | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md b/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md index fac4e898..e7d3ff62 100644 --- a/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md +++ b/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md @@ -26,6 +26,7 @@ Introduce `FrameComposer` as a first-class hardware-side subsystem and move cano - optional cache; - optional resolver; - owned `SpriteController`. +- Preserve scene-layer metadata naming aligned with the world path contract, including `parallax_factor` as the canonical per-layer camera multiplier field. - Aggregate `FrameComposer` inside `Hardware`. - Expose the minimum driver-facing surface required for subsequent plans. diff --git a/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md b/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md index 33c3693d..1b83928d 100644 --- a/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md +++ b/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md @@ -16,6 +16,7 @@ Implement the `FrameComposer` scene-binding contract, minimal camera state, and `DEC-0014` locks scene activation around `bind_scene(scene_bank_id)` with `SceneBankPoolAccess`, pointer-based access only, and `scene_bank_id + Arc` retained inside `FrameComposer`. The same decision also requires `FrameComposer` to remain tile-size agnostic and to preserve canonical per-layer `tile_size`, including `8x8`. +For the scene-layer motion contract, this plan treats `parallax_factor` as the canonical field name for the per-layer camera multiplier. ## Scope @@ -92,6 +93,7 @@ Align cache/resolver lifetime with the active scene contract. - On unbind: - discard cache/resolver and invalidate the world path. - Any initialization must derive layer math from the bound scene tile sizes instead of assuming `16x16`. +- Any layer-camera math or related contract references must use `parallax_factor` terminology rather than generic `motion` naming. **File(s):** - `crates/console/prometeu-drivers/src/frame_composer.rs` @@ -104,6 +106,7 @@ Align cache/resolver lifetime with the active scene contract. - scene status reflects no-scene and active-scene states. - camera coordinates are stored as top-left pixel-space values. - bind/unbind remains valid for scenes whose layers use `8x8` tiles. +- scene binding and camera-facing contracts preserve `parallax_factor` as the canonical layer field name. ### Integration Tests - `FrameComposer` can resolve a scene from the pool and survive no-scene operation. @@ -119,6 +122,7 @@ Align cache/resolver lifetime with the active scene contract. - [ ] Camera contract is implemented as `i32` top-left viewport coordinates. - [ ] Cache/resolver lifetime follows scene bind/unbind. - [ ] Scene bind/cache/resolver setup preserves canonical per-layer tile sizes, including `8x8`. +- [ ] Scene-layer camera multiplier naming is aligned on `parallax_factor`. ## Dependencies diff --git a/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md b/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md index 9f1064f4..925517e8 100644 --- a/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md +++ b/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md @@ -16,6 +16,7 @@ Connect `FrameComposer` to `SceneViewportResolver`, apply cache refreshes inside `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. `DEC-0014` also requires the world path to remain tile-size agnostic, with explicit support for `8x8`, `16x16`, and `32x32` scene-layer tile sizes. +For per-layer camera scaling, this plan treats `parallax_factor` as the canonical scene-layer field name. ## Scope @@ -43,6 +44,7 @@ Move cache-refresh orchestration fully into `FrameComposer`. - apply them to `SceneViewportCache` - Keep `Gfx` unaware of refresh semantics. - Ensure resolver and refresh math follow the bound layer `tile_size` values rather than any fixed `16x16` default. +- Ensure per-layer camera math is expressed through `parallax_factor` naming in the resolver/cache path. **File(s):** - `crates/console/prometeu-drivers/src/frame_composer.rs` @@ -103,6 +105,7 @@ Protect the two canonical frame modes. - `render_frame()` with a scene applies resolver refreshes before composition. - cache refresh requests are applied by `FrameComposer`, not `Gfx`. - `render_frame()` with an `8x8` scene uses resolver/cache math derived from layer tile size rather than a `16x16` assumption. +- Resolver/cache-facing tests use `parallax_factor` terminology for per-layer camera scaling. ### Integration Tests - scene bind + camera set + sprite emission + `render_frame()` produces the expected composed frame. @@ -119,6 +122,7 @@ Protect the two canonical frame modes. - [ ] No-scene `sprites + fades` behavior remains valid. - [ ] `Gfx` remains backend-only for this path. - [ ] The world path is explicitly covered for `8x8` scenes without `16x16`-specific assumptions. +- [ ] Resolver/cache/frame-path terminology is aligned on `parallax_factor` for scene-layer camera scaling. ## Dependencies -- 2.47.2 From ed05f337ce93beec298cb77723a4cf3bb779f246 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 13:19:03 +0100 Subject: [PATCH 04/21] implements PLN-0017 --- crates/console/prometeu-drivers/src/asset.rs | 26 +-- .../prometeu-drivers/src/frame_composer.rs | 197 ++++++++++++++++++ crates/console/prometeu-drivers/src/gfx.rs | 6 +- .../console/prometeu-drivers/src/hardware.rs | 32 ++- crates/console/prometeu-drivers/src/lib.rs | 2 + crates/console/prometeu-hal/src/scene_bank.rs | 6 +- .../console/prometeu-hal/src/scene_layer.rs | 12 +- .../prometeu-hal/src/scene_viewport_cache.rs | 4 +- .../src/scene_viewport_resolver.rs | 14 +- 9 files changed, 261 insertions(+), 38 deletions(-) create mode 100644 crates/console/prometeu-drivers/src/frame_composer.rs diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index d6541337..99d33cf1 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -14,7 +14,7 @@ use prometeu_hal::glyph::Glyph; use prometeu_hal::glyph_bank::{GlyphBank, TileSize}; use prometeu_hal::sample::Sample; use prometeu_hal::scene_bank::SceneBank; -use prometeu_hal::scene_layer::{MotionFactor, SceneLayer}; +use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::sound_bank::SoundBank; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; @@ -748,7 +748,7 @@ impl AssetManager { active: false, glyph_bank_id: 0, tile_size: TileSize::Size8, - motion_factor: MotionFactor { x: 1.0, y: 1.0 }, + parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width: 0, height: 0, tiles: Vec::new() }, }); let mut layers = layers; @@ -770,20 +770,20 @@ impl AssetManager { 32 => TileSize::Size32, other => return Err(format!("Invalid SCENE tile size: {}", other)), }; - let motion_factor_x = f32::from_le_bytes([ + let parallax_factor_x = f32::from_le_bytes([ buffer[offset + 4], buffer[offset + 5], buffer[offset + 6], buffer[offset + 7], ]); - let motion_factor_y = f32::from_le_bytes([ + let parallax_factor_y = f32::from_le_bytes([ buffer[offset + 8], buffer[offset + 9], buffer[offset + 10], buffer[offset + 11], ]); - if !motion_factor_x.is_finite() || !motion_factor_y.is_finite() { - return Err("Invalid SCENE motion_factor".to_string()); + if !parallax_factor_x.is_finite() || !parallax_factor_y.is_finite() { + return Err("Invalid SCENE parallax_factor".to_string()); } let width = u32::from_le_bytes([ @@ -847,7 +847,7 @@ impl AssetManager { active: (flags & 0b0000_0001) != 0, glyph_bank_id, tile_size, - motion_factor: MotionFactor { x: motion_factor_x, y: motion_factor_y }, + parallax_factor: ParallaxFactor { x: parallax_factor_x, y: parallax_factor_y }, tilemap: TileMap { width, height, tiles }, }; } @@ -1105,7 +1105,7 @@ mod tests { SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1, }; use prometeu_hal::glyph::Glyph; - use prometeu_hal::scene_layer::{MotionFactor, SceneLayer}; + use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; @@ -1144,11 +1144,11 @@ mod tests { fn test_scene() -> SceneBank { let make_layer = - |glyph_bank_id: u8, motion_x: f32, motion_y: f32, tile_size: TileSize| SceneLayer { + |glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, tile_size: TileSize| SceneLayer { active: glyph_bank_id != 3, glyph_bank_id, tile_size, - motion_factor: MotionFactor { x: motion_x, y: motion_y }, + parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y }, tilemap: TileMap { width: 2, height: 2, @@ -1227,8 +1227,8 @@ mod tests { data.push(layer.glyph_bank_id); data.push(layer.tile_size as u8); data.push(0); - data.extend_from_slice(&layer.motion_factor.x.to_le_bytes()); - data.extend_from_slice(&layer.motion_factor.y.to_le_bytes()); + data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes()); + data.extend_from_slice(&layer.parallax_factor.y.to_le_bytes()); data.extend_from_slice(&(layer.tilemap.width as u32).to_le_bytes()); data.extend_from_slice(&(layer.tilemap.height as u32).to_le_bytes()); data.extend_from_slice(&(layer.tilemap.tiles.len() as u32).to_le_bytes()); @@ -1359,7 +1359,7 @@ mod tests { let decoded = AssetManager::decode_scene_bank_from_buffer(&entry, &data).expect("scene"); assert_eq!(decoded.layers[1].glyph_bank_id, 1); - assert_eq!(decoded.layers[1].motion_factor.x, 0.5); + assert_eq!(decoded.layers[1].parallax_factor.x, 0.5); assert_eq!(decoded.layers[2].tile_size, TileSize::Size32); assert_eq!(decoded.layers[0].tilemap.tiles[1].flip_x, true); assert_eq!(decoded.layers[2].tilemap.tiles[2].flip_y, true); diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs new file mode 100644 index 00000000..1a7b77cc --- /dev/null +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -0,0 +1,197 @@ +use crate::memory_banks::SceneBankPoolAccess; +use prometeu_hal::glyph::Glyph; +use prometeu_hal::scene_bank::SceneBank; +use prometeu_hal::scene_viewport_cache::SceneViewportCache; +use prometeu_hal::scene_viewport_resolver::SceneViewportResolver; +use prometeu_hal::sprite::Sprite; +use std::sync::Arc; + +const EMPTY_SPRITE: Sprite = Sprite { + glyph: Glyph { glyph_id: 0, palette_id: 0 }, + x: 0, + y: 0, + bank_id: 0, + active: false, + flip_x: false, + flip_y: false, + priority: 0, +}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SceneStatus { + #[default] + Unbound, +} + +#[derive(Clone, Debug)] +pub struct SpriteController { + sprites: [Sprite; 512], +} + +impl Default for SpriteController { + fn default() -> Self { + Self::new() + } +} + +impl SpriteController { + pub fn new() -> Self { + Self { sprites: [EMPTY_SPRITE; 512] } + } + + pub fn sprites(&self) -> &[Sprite; 512] { + &self.sprites + } + + pub fn sprites_mut(&mut self) -> &mut [Sprite; 512] { + &mut self.sprites + } +} + +pub struct FrameComposer { + scene_bank_pool: Arc, + viewport_width_px: usize, + viewport_height_px: usize, + active_scene_id: Option, + active_scene: Option>, + scene_status: SceneStatus, + camera_x_px: i32, + camera_y_px: i32, + cache: Option, + resolver: Option, + sprite_controller: SpriteController, +} + +impl FrameComposer { + pub fn new( + viewport_width_px: usize, + viewport_height_px: usize, + scene_bank_pool: Arc, + ) -> Self { + Self { + scene_bank_pool, + viewport_width_px, + viewport_height_px, + active_scene_id: None, + active_scene: None, + scene_status: SceneStatus::Unbound, + camera_x_px: 0, + camera_y_px: 0, + cache: None, + resolver: None, + sprite_controller: SpriteController::new(), + } + } + + pub fn viewport_size(&self) -> (usize, usize) { + (self.viewport_width_px, self.viewport_height_px) + } + + pub fn scene_bank_pool(&self) -> &Arc { + &self.scene_bank_pool + } + + pub fn scene_bank_slot(&self, slot: usize) -> Option> { + self.scene_bank_pool.scene_bank_slot(slot) + } + + pub fn scene_bank_slot_count(&self) -> usize { + self.scene_bank_pool.scene_bank_slot_count() + } + + pub fn active_scene_id(&self) -> Option { + self.active_scene_id + } + + pub fn active_scene(&self) -> Option<&Arc> { + self.active_scene.as_ref() + } + + pub fn scene_status(&self) -> SceneStatus { + self.scene_status + } + + pub fn camera(&self) -> (i32, i32) { + (self.camera_x_px, self.camera_y_px) + } + + pub fn cache(&self) -> Option<&SceneViewportCache> { + self.cache.as_ref() + } + + pub fn resolver(&self) -> Option<&SceneViewportResolver> { + self.resolver.as_ref() + } + + pub fn sprite_controller(&self) -> &SpriteController { + &self.sprite_controller + } + + pub fn sprite_controller_mut(&mut self) -> &mut SpriteController { + &mut self.sprite_controller + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::memory_banks::{MemoryBanks, SceneBankPoolInstaller}; + use prometeu_hal::glyph::Glyph; + use prometeu_hal::glyph_bank::TileSize; + use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; + use prometeu_hal::tile::Tile; + use prometeu_hal::tilemap::TileMap; + + fn make_scene() -> SceneBank { + let layer = SceneLayer { + active: true, + glyph_bank_id: 1, + tile_size: TileSize::Size8, + parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 }, + tilemap: TileMap { + width: 2, + height: 2, + tiles: vec![ + Tile { + active: true, + glyph: Glyph { glyph_id: 9, palette_id: 1 }, + flip_x: false, + flip_y: false, + }; + 4 + ], + }, + }; + + SceneBank { layers: std::array::from_fn(|_| layer.clone()) } + } + + #[test] + fn frame_composer_starts_unbound_with_empty_owned_state() { + let frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + + assert_eq!(frame_composer.viewport_size(), (320, 180)); + assert_eq!(frame_composer.active_scene_id(), None); + assert!(frame_composer.active_scene().is_none()); + assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound); + assert_eq!(frame_composer.camera(), (0, 0)); + assert!(frame_composer.cache().is_none()); + assert!(frame_composer.resolver().is_none()); + assert_eq!(frame_composer.sprite_controller().sprites().len(), 512); + assert!(frame_composer.sprite_controller().sprites().iter().all(|sprite| !sprite.active)); + } + + #[test] + fn frame_composer_exposes_shared_scene_bank_access() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_scene_bank(3, Arc::new(make_scene())); + + let frame_composer = FrameComposer::new(320, 180, banks); + let scene = + frame_composer.scene_bank_slot(3).expect("scene bank slot 3 should be resident"); + + assert_eq!(frame_composer.scene_bank_slot_count(), 16); + assert_eq!(scene.layers[0].tile_size, TileSize::Size8); + assert_eq!(scene.layers[0].parallax_factor.y, 0.5); + } +} diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 26d09ebf..9462d8dc 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -822,7 +822,7 @@ mod tests { use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks}; use prometeu_hal::glyph_bank::TileSize; use prometeu_hal::scene_bank::SceneBank; - use prometeu_hal::scene_layer::{MotionFactor, SceneLayer}; + use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::scene_viewport_cache::SceneViewportCache; use prometeu_hal::scene_viewport_resolver::SceneViewportResolver; use prometeu_hal::tile::Tile; @@ -853,7 +853,7 @@ mod tests { active: true, glyph_bank_id, tile_size: TileSize::Size8, - motion_factor: MotionFactor { x: 1.0, y: 1.0 }, + parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width, height, @@ -875,7 +875,7 @@ mod tests { active: false, glyph_bank_id, tile_size: TileSize::Size8, - motion_factor: MotionFactor { x: 1.0, y: 1.0 }, + parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] }, } } diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 3d7a2194..e1ef2d3f 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -1,9 +1,10 @@ use crate::asset::AssetManager; use crate::audio::Audio; +use crate::frame_composer::FrameComposer; use crate::gfx::Gfx; use crate::memory_banks::{ - GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller, - SoundBankPoolAccess, SoundBankPoolInstaller, + GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess, + SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller, }; use crate::pad::Pad; use crate::touch::Touch; @@ -26,6 +27,8 @@ use std::sync::Arc; pub struct Hardware { /// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps. pub gfx: Gfx, + /// Canonical frame orchestration owner for scene/camera/cache/resolver/sprites. + pub frame_composer: FrameComposer, /// The Sound Processing Unit (SPU). Manages sample playback and volume control. pub audio: Audio, /// The standard digital gamepad. Provides state for D-Pad, face buttons, and triggers. @@ -98,6 +101,11 @@ impl Hardware { Self::H, Arc::clone(&memory_banks) as Arc, ), + frame_composer: FrameComposer::new( + Self::W, + Self::H, + Arc::clone(&memory_banks) as Arc, + ), audio: Audio::new(Arc::clone(&memory_banks) as Arc), pad: Pad::default(), touch: Touch::default(), @@ -122,7 +130,7 @@ mod tests { use prometeu_hal::glyph::Glyph; use prometeu_hal::glyph_bank::{GlyphBank, TileSize}; use prometeu_hal::scene_bank::SceneBank; - use prometeu_hal::scene_layer::{MotionFactor, SceneLayer}; + use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::scene_viewport_cache::SceneViewportCache; use prometeu_hal::scene_viewport_resolver::SceneViewportResolver; use prometeu_hal::tile::Tile; @@ -142,7 +150,7 @@ mod tests { active: true, glyph_bank_id: 0, tile_size: TileSize::Size8, - motion_factor: MotionFactor { x: 1.0, y: 1.0 }, + parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width: 4, height: 4, @@ -182,4 +190,20 @@ mod tests { assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw()); } + + #[test] + fn hardware_constructs_frame_composer_with_shared_scene_bank_access() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_scene_bank(2, Arc::new(make_scene())); + + let hardware = Hardware::new_with_memory_banks(banks); + let scene = hardware + .frame_composer + .scene_bank_slot(2) + .expect("scene bank slot 2 should be resident"); + + assert_eq!(hardware.frame_composer.viewport_size(), (Hardware::W, Hardware::H)); + assert_eq!(hardware.frame_composer.scene_bank_slot_count(), 16); + assert_eq!(scene.layers[0].tile_size, TileSize::Size8); + } } diff --git a/crates/console/prometeu-drivers/src/lib.rs b/crates/console/prometeu-drivers/src/lib.rs index f9d82bc5..9a088424 100644 --- a/crates/console/prometeu-drivers/src/lib.rs +++ b/crates/console/prometeu-drivers/src/lib.rs @@ -1,5 +1,6 @@ mod asset; mod audio; +mod frame_composer; mod gfx; pub mod hardware; mod memory_banks; @@ -8,6 +9,7 @@ mod touch; pub use crate::asset::AssetManager; pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; +pub use crate::frame_composer::{FrameComposer, SceneStatus, SpriteController}; pub use crate::gfx::Gfx; pub use crate::memory_banks::{ GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess, diff --git a/crates/console/prometeu-hal/src/scene_bank.rs b/crates/console/prometeu-hal/src/scene_bank.rs index c5f4b296..6abb47d5 100644 --- a/crates/console/prometeu-hal/src/scene_bank.rs +++ b/crates/console/prometeu-hal/src/scene_bank.rs @@ -10,16 +10,16 @@ mod tests { use super::*; use crate::glyph::Glyph; use crate::glyph_bank::TileSize; - use crate::scene_layer::MotionFactor; + use crate::scene_layer::ParallaxFactor; use crate::tile::Tile; use crate::tilemap::TileMap; - fn layer(glyph_bank_id: u8, motion_x: f32, motion_y: f32, glyph_id: u16) -> SceneLayer { + fn layer(glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer { SceneLayer { active: true, glyph_bank_id, tile_size: TileSize::Size16, - motion_factor: MotionFactor { x: motion_x, y: motion_y }, + parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y }, tilemap: TileMap { width: 1, height: 1, diff --git a/crates/console/prometeu-hal/src/scene_layer.rs b/crates/console/prometeu-hal/src/scene_layer.rs index a7c448f3..90feec93 100644 --- a/crates/console/prometeu-hal/src/scene_layer.rs +++ b/crates/console/prometeu-hal/src/scene_layer.rs @@ -2,7 +2,7 @@ use crate::glyph_bank::TileSize; use crate::tilemap::TileMap; #[derive(Clone, Copy, Debug)] -pub struct MotionFactor { +pub struct ParallaxFactor { pub x: f32, pub y: f32, } @@ -12,7 +12,7 @@ pub struct SceneLayer { pub active: bool, pub glyph_bank_id: u8, pub tile_size: TileSize, - pub motion_factor: MotionFactor, + pub parallax_factor: ParallaxFactor, pub tilemap: TileMap, } @@ -23,12 +23,12 @@ mod tests { use crate::tile::Tile; #[test] - fn scene_layer_preserves_motion_factor_and_tilemap_ownership() { + fn scene_layer_preserves_parallax_factor_and_tilemap_ownership() { let layer = SceneLayer { active: true, glyph_bank_id: 7, tile_size: TileSize::Size16, - motion_factor: MotionFactor { x: 0.5, y: 0.75 }, + parallax_factor: ParallaxFactor { x: 0.5, y: 0.75 }, tilemap: TileMap { width: 2, height: 1, @@ -50,8 +50,8 @@ mod tests { }; assert_eq!(layer.glyph_bank_id, 7); - assert_eq!(layer.motion_factor.x, 0.5); - assert_eq!(layer.motion_factor.y, 0.75); + assert_eq!(layer.parallax_factor.x, 0.5); + assert_eq!(layer.parallax_factor.y, 0.75); assert_eq!(layer.tilemap.width, 2); assert_eq!(layer.tilemap.tiles[1].glyph.glyph_id, 22); assert!(layer.tilemap.tiles[1].flip_x); diff --git a/crates/console/prometeu-hal/src/scene_viewport_cache.rs b/crates/console/prometeu-hal/src/scene_viewport_cache.rs index c0c3bcdb..4c2ee614 100644 --- a/crates/console/prometeu-hal/src/scene_viewport_cache.rs +++ b/crates/console/prometeu-hal/src/scene_viewport_cache.rs @@ -270,7 +270,7 @@ mod tests { use super::*; use crate::glyph::Glyph; use crate::glyph_bank::TileSize; - use crate::scene_layer::MotionFactor; + use crate::scene_layer::ParallaxFactor; use crate::tile::Tile; use crate::tilemap::TileMap; @@ -295,7 +295,7 @@ mod tests { active: true, glyph_bank_id, tile_size: TileSize::Size16, - motion_factor: MotionFactor { x: 1.0, y: 1.0 }, + parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width: 4, height: 4, tiles }, } } diff --git a/crates/console/prometeu-hal/src/scene_viewport_resolver.rs b/crates/console/prometeu-hal/src/scene_viewport_resolver.rs index 41fad0f2..a9e99902 100644 --- a/crates/console/prometeu-hal/src/scene_viewport_resolver.rs +++ b/crates/console/prometeu-hal/src/scene_viewport_resolver.rs @@ -96,8 +96,8 @@ impl SceneViewportResolver { let layer_inputs: [(i32, i32, i32, i32, i32); 4] = std::array::from_fn(|i| { let layer = &scene.layers[i]; let tile_size_px = layer.tile_size as i32; - let layer_camera_x_px = ((camera_x_px as f32) * layer.motion_factor.x).floor() as i32; - let layer_camera_y_px = ((camera_y_px as f32) * layer.motion_factor.y).floor() as i32; + let layer_camera_x_px = ((camera_x_px as f32) * layer.parallax_factor.x).floor() as i32; + let layer_camera_y_px = ((camera_y_px as f32) * layer.parallax_factor.y).floor() as i32; let layer_center_x_px = layer_camera_x_px + self.viewport_width_px / 2; let layer_center_y_px = layer_camera_y_px + self.viewport_height_px / 2; ( @@ -388,14 +388,14 @@ mod tests { use super::*; use crate::glyph::Glyph; use crate::glyph_bank::TileSize; - use crate::scene_layer::{MotionFactor, SceneLayer}; + use crate::scene_layer::{ParallaxFactor, SceneLayer}; use crate::tile::Tile; use crate::tilemap::TileMap; fn make_layer( tile_size: TileSize, - motion_x: f32, - motion_y: f32, + parallax_x: f32, + parallax_y: f32, width: usize, height: usize, ) -> SceneLayer { @@ -413,7 +413,7 @@ mod tests { active: true, glyph_bank_id: 1, tile_size, - motion_factor: MotionFactor { x: motion_x, y: motion_y }, + parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y }, tilemap: TileMap { width, height, tiles }, } } @@ -443,7 +443,7 @@ mod tests { } #[test] - fn per_layer_copy_requests_follow_motion_factor() { + fn per_layer_copy_requests_follow_parallax_factor() { let scene = make_scene(); let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20); -- 2.47.2 From 5a0476e8b0eaee2fc31992246de3fdc1d1f0bc39 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 13:24:25 +0100 Subject: [PATCH 05/21] implements PLN-0018 --- .../prometeu-drivers/src/frame_composer.rs | 232 +++++++++++++++++- crates/console/prometeu-drivers/src/gfx.rs | 130 +++++++--- crates/console/prometeu-hal/src/gfx_bridge.rs | 1 + crates/console/prometeu-hal/src/sprite.rs | 1 + .../src/virtual_machine_runtime/dispatch.rs | 1 + 5 files changed, 323 insertions(+), 42 deletions(-) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index 1a7b77cc..f5b83c8e 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -10,6 +10,7 @@ const EMPTY_SPRITE: Sprite = Sprite { glyph: Glyph { glyph_id: 0, palette_id: 0 }, x: 0, y: 0, + layer: 0, bank_id: 0, active: false, flip_x: false, @@ -26,6 +27,10 @@ pub enum SceneStatus { #[derive(Clone, Debug)] pub struct SpriteController { sprites: [Sprite; 512], + sprite_count: usize, + frame_counter: u64, + dropped_sprites: usize, + layer_buckets: [Vec; 4], } impl Default for SpriteController { @@ -36,7 +41,13 @@ impl Default for SpriteController { impl SpriteController { pub fn new() -> Self { - Self { sprites: [EMPTY_SPRITE; 512] } + Self { + sprites: [EMPTY_SPRITE; 512], + sprite_count: 0, + frame_counter: 0, + dropped_sprites: 0, + layer_buckets: std::array::from_fn(|_| Vec::with_capacity(128)), + } } pub fn sprites(&self) -> &[Sprite; 512] { @@ -46,6 +57,57 @@ impl SpriteController { pub fn sprites_mut(&mut self) -> &mut [Sprite; 512] { &mut self.sprites } + + pub fn begin_frame(&mut self) { + self.frame_counter = self.frame_counter.wrapping_add(1); + self.sprite_count = 0; + self.dropped_sprites = 0; + for bucket in &mut self.layer_buckets { + bucket.clear(); + } + } + + pub fn emit_sprite(&mut self, mut sprite: Sprite) -> bool { + let Some(bucket) = self.layer_buckets.get_mut(sprite.layer as usize) else { + self.dropped_sprites += 1; + return false; + }; + if self.sprite_count >= self.sprites.len() { + self.dropped_sprites += 1; + return false; + } + + sprite.active = true; + let index = self.sprite_count; + self.sprites[index] = sprite; + self.sprite_count += 1; + bucket.push(index); + true + } + + pub fn sprite_count(&self) -> usize { + self.sprite_count + } + + pub fn frame_counter(&self) -> u64 { + self.frame_counter + } + + pub fn dropped_sprites(&self) -> usize { + self.dropped_sprites + } + + pub fn ordered_sprites(&self) -> Vec { + let mut ordered = Vec::with_capacity(self.sprite_count); + for bucket in &self.layer_buckets { + let mut indices = bucket.clone(); + indices.sort_by_key(|&index| self.sprites[index].priority); + for index in indices { + ordered.push(self.sprites[index]); + } + } + ordered + } } pub struct FrameComposer { @@ -130,13 +192,24 @@ impl FrameComposer { pub fn sprite_controller_mut(&mut self) -> &mut SpriteController { &mut self.sprite_controller } + + pub fn begin_frame(&mut self) { + self.sprite_controller.begin_frame(); + } + + pub fn emit_sprite(&mut self, sprite: Sprite) -> bool { + self.sprite_controller.emit_sprite(sprite) + } + + pub fn ordered_sprites(&self) -> Vec { + self.sprite_controller.ordered_sprites() + } } #[cfg(test)] mod tests { use super::*; use crate::memory_banks::{MemoryBanks, SceneBankPoolInstaller}; - use prometeu_hal::glyph::Glyph; use prometeu_hal::glyph_bank::TileSize; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::tile::Tile; @@ -178,7 +251,8 @@ mod tests { assert!(frame_composer.cache().is_none()); assert!(frame_composer.resolver().is_none()); assert_eq!(frame_composer.sprite_controller().sprites().len(), 512); - assert!(frame_composer.sprite_controller().sprites().iter().all(|sprite| !sprite.active)); + assert_eq!(frame_composer.sprite_controller().sprite_count(), 0); + assert_eq!(frame_composer.sprite_controller().dropped_sprites(), 0); } #[test] @@ -194,4 +268,156 @@ mod tests { assert_eq!(scene.layers[0].tile_size, TileSize::Size8); assert_eq!(scene.layers[0].parallax_factor.y, 0.5); } + + #[test] + fn sprite_controller_begin_frame_resets_sprite_count_and_buckets() { + let mut controller = SpriteController::new(); + let emitted = controller.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 1, palette_id: 2 }, + x: 4, + y: 5, + layer: 2, + bank_id: 3, + active: false, + flip_x: false, + flip_y: false, + priority: 1, + }); + assert!(emitted); + + controller.begin_frame(); + + assert_eq!(controller.frame_counter(), 1); + assert_eq!(controller.sprite_count(), 0); + assert_eq!(controller.dropped_sprites(), 0); + assert!(controller.ordered_sprites().is_empty()); + } + + #[test] + fn sprite_controller_orders_by_layer_then_priority_then_fifo() { + let mut controller = SpriteController::new(); + controller.begin_frame(); + + assert!(controller.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 10, palette_id: 0 }, + x: 0, + y: 0, + layer: 1, + bank_id: 0, + active: false, + flip_x: false, + flip_y: false, + priority: 2, + })); + assert!(controller.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 11, palette_id: 0 }, + x: 0, + y: 0, + layer: 0, + bank_id: 0, + active: false, + flip_x: false, + flip_y: false, + priority: 3, + })); + assert!(controller.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 12, palette_id: 0 }, + x: 0, + y: 0, + layer: 1, + bank_id: 0, + active: false, + flip_x: false, + flip_y: false, + priority: 1, + })); + assert!(controller.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 13, palette_id: 0 }, + x: 0, + y: 0, + layer: 1, + bank_id: 0, + active: false, + flip_x: false, + flip_y: false, + priority: 2, + })); + + let ordered = controller.ordered_sprites(); + let ordered_ids: Vec = ordered.iter().map(|sprite| sprite.glyph.glyph_id).collect(); + + assert_eq!(ordered_ids, vec![11, 12, 10, 13]); + assert!(ordered.iter().all(|sprite| sprite.active)); + } + + #[test] + fn sprite_controller_drops_overflow_without_panicking() { + let mut controller = SpriteController::new(); + controller.begin_frame(); + + for glyph_id in 0..512 { + assert!(controller.emit_sprite(Sprite { + glyph: Glyph { glyph_id, palette_id: 0 }, + x: 0, + y: 0, + layer: 0, + bank_id: 0, + active: false, + flip_x: false, + flip_y: false, + priority: 0, + })); + } + + let overflowed = controller.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 999, palette_id: 0 }, + x: 0, + y: 0, + layer: 0, + bank_id: 0, + active: false, + flip_x: false, + flip_y: false, + priority: 0, + }); + + assert!(!overflowed); + assert_eq!(controller.sprite_count(), 512); + assert_eq!(controller.dropped_sprites(), 1); + } + + #[test] + fn frame_composer_emits_ordered_sprites_for_rendering() { + let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + frame_composer.begin_frame(); + + assert!(frame_composer.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 21, palette_id: 0 }, + x: 0, + y: 0, + layer: 2, + bank_id: 1, + active: false, + flip_x: false, + flip_y: false, + priority: 1, + })); + assert!(frame_composer.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 20, palette_id: 0 }, + x: 0, + y: 0, + layer: 1, + bank_id: 1, + active: false, + flip_x: false, + flip_y: false, + priority: 0, + })); + + let ordered = frame_composer.ordered_sprites(); + + assert_eq!(ordered.len(), 2); + assert_eq!(ordered[0].glyph.glyph_id, 20); + assert_eq!(ordered[1].glyph.glyph_id, 21); + } } diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 9462d8dc..e78b3e4c 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -71,8 +71,10 @@ pub struct Gfx { /// Target color for the HUD fade effect. pub hud_fade_color: Color, - /// Internal cache used to sort sprites into priority groups to optimize rendering. - priority_buckets: [Vec; 5], + /// Internal sprite count for the current frame state. + sprite_count: usize, + /// Internal cache used to sort sprites by layer while keeping stable priority order. + layer_buckets: [Vec; 4], } const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7]; @@ -213,6 +215,9 @@ impl GfxBridge for Gfx { fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) { self.render_scene_from_cache(cache, update) } + fn load_frame_sprites(&mut self, sprites: &[Sprite]) { + self.load_frame_sprites(sprites) + } fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { self.draw_text(x, y, text, color) } @@ -224,6 +229,7 @@ impl GfxBridge for Gfx { &self.sprites[index] } fn sprite_mut(&mut self, index: usize) -> &mut Sprite { + self.sprite_count = self.sprite_count.max(index.saturating_add(1)).min(self.sprites.len()); &mut self.sprites[index] } @@ -262,6 +268,7 @@ impl Gfx { glyph: EMPTY_GLYPH, x: 0, y: 0, + layer: 0, bank_id: 0, active: false, flip_x: false, @@ -277,12 +284,12 @@ impl Gfx { back: vec![0; len], glyph_banks, sprites: [EMPTY_SPRITE; 512], + sprite_count: 0, scene_fade_level: 31, scene_fade_color: Color::BLACK, hud_fade_level: 31, hud_fade_color: Color::BLACK, - priority_buckets: [ - Vec::with_capacity(128), + layer_buckets: [ Vec::with_capacity(128), Vec::with_capacity(128), Vec::with_capacity(128), @@ -530,23 +537,33 @@ impl Gfx { std::mem::swap(&mut self.front, &mut self.back); } + pub fn load_frame_sprites(&mut self, sprites: &[Sprite]) { + self.sprite_count = sprites.len().min(self.sprites.len()); + for (index, sprite) in sprites.iter().copied().take(self.sprites.len()).enumerate() { + self.sprites[index] = Sprite { active: true, ..sprite }; + } + for sprite in self.sprites.iter_mut().skip(self.sprite_count) { + sprite.active = false; + } + } + /// The main rendering pipeline. /// /// This method composes the final frame by rasterizing layers and sprites in the /// correct priority order into the back buffer. /// Follows the hardware model where layers and sprites are composed every frame. pub fn render_all(&mut self) { - self.populate_priority_buckets(); - - // 1. Priority 0 sprites: drawn at the very back, behind everything else. - Self::draw_bucket_on_buffer( - &mut self.back, - self.w, - self.h, - &self.priority_buckets[0], - &self.sprites, - &*self.glyph_banks, - ); + self.populate_layer_buckets(); + for bucket in &self.layer_buckets { + Self::draw_bucket_on_buffer( + &mut self.back, + self.w, + self.h, + bucket, + &self.sprites, + &*self.glyph_banks, + ); + } // 2. Scene-only fallback path: sprites and fades still work even before a // cache-backed world composition request is issued for the frame. @@ -563,18 +580,17 @@ impl Gfx { /// plus sprite state and fade controls. pub fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) { self.back.fill(Color::BLACK.raw()); - self.populate_priority_buckets(); - - Self::draw_bucket_on_buffer( - &mut self.back, - self.w, - self.h, - &self.priority_buckets[0], - &self.sprites, - &*self.glyph_banks, - ); + self.populate_layer_buckets(); for layer_index in 0..cache.layers.len() { + Self::draw_bucket_on_buffer( + &mut self.back, + self.w, + self.h, + &self.layer_buckets[layer_index], + &self.sprites, + &*self.glyph_banks, + ); Self::draw_cache_layer_to_buffer( &mut self.back, self.w, @@ -583,31 +599,26 @@ impl Gfx { &update.copy_requests[layer_index], &*self.glyph_banks, ); - - Self::draw_bucket_on_buffer( - &mut self.back, - self.w, - self.h, - &self.priority_buckets[layer_index + 1], - &self.sprites, - &*self.glyph_banks, - ); } Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color); Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color); } - fn populate_priority_buckets(&mut self) { - for bucket in self.priority_buckets.iter_mut() { + fn populate_layer_buckets(&mut self) { + for bucket in self.layer_buckets.iter_mut() { bucket.clear(); } - for (idx, sprite) in self.sprites.iter().enumerate() { - if sprite.active && sprite.priority < 5 { - self.priority_buckets[sprite.priority as usize].push(idx); + for (idx, sprite) in self.sprites.iter().take(self.sprite_count).enumerate() { + if sprite.active && (sprite.layer as usize) < self.layer_buckets.len() { + self.layer_buckets[sprite.layer as usize].push(idx); } } + + for bucket in self.layer_buckets.iter_mut() { + bucket.sort_by_key(|&idx| self.sprites[idx].priority); + } } fn draw_cache_layer_to_buffer( @@ -1004,6 +1015,7 @@ mod tests { glyph: Glyph { glyph_id: 0, palette_id: 4 }, x: 0, y: 0, + layer: 0, bank_id: 0, active: true, flip_x: false, @@ -1014,17 +1026,57 @@ mod tests { glyph: Glyph { glyph_id: 0, palette_id: 4 }, x: 0, y: 0, + layer: 2, bank_id: 0, active: true, flip_x: false, flip_y: false, priority: 2, }; + gfx.sprite_count = 2; gfx.render_scene_from_cache(&cache, &update); assert_eq!(gfx.back[0], Color::BLUE.raw()); } + + #[test] + fn load_frame_sprites_replaces_slot_first_submission_for_render_state() { + let banks = Arc::new(MemoryBanks::new()); + let mut gfx = Gfx::new(16, 16, banks as Arc); + + gfx.load_frame_sprites(&[ + Sprite { + glyph: Glyph { glyph_id: 1, palette_id: 2 }, + x: 2, + y: 3, + layer: 1, + bank_id: 4, + active: false, + flip_x: true, + flip_y: false, + priority: 7, + }, + Sprite { + glyph: Glyph { glyph_id: 5, palette_id: 6 }, + x: 7, + y: 8, + layer: 3, + bank_id: 9, + active: false, + flip_x: false, + flip_y: true, + priority: 1, + }, + ]); + + assert_eq!(gfx.sprite_count, 2); + assert!(gfx.sprites[0].active); + assert!(gfx.sprites[1].active); + assert!(!gfx.sprites[2].active); + assert_eq!(gfx.sprites[0].layer, 1); + assert_eq!(gfx.sprites[1].glyph.glyph_id, 5); + } } /// Blends in RGB565 per channel with saturation. diff --git a/crates/console/prometeu-hal/src/gfx_bridge.rs b/crates/console/prometeu-hal/src/gfx_bridge.rs index 1c677ab4..b000d20d 100644 --- a/crates/console/prometeu-hal/src/gfx_bridge.rs +++ b/crates/console/prometeu-hal/src/gfx_bridge.rs @@ -50,6 +50,7 @@ pub trait GfxBridge { fn present(&mut self); fn render_all(&mut self); fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate); + fn load_frame_sprites(&mut self, sprites: &[Sprite]); fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color); fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color); diff --git a/crates/console/prometeu-hal/src/sprite.rs b/crates/console/prometeu-hal/src/sprite.rs index 1d5f141f..e38ae920 100644 --- a/crates/console/prometeu-hal/src/sprite.rs +++ b/crates/console/prometeu-hal/src/sprite.rs @@ -5,6 +5,7 @@ pub struct Sprite { pub glyph: Glyph, pub x: i32, pub y: i32, + pub layer: u8, pub bank_id: u8, pub active: bool, pub flip_x: bool, diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs index e6037679..86184215 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -166,6 +166,7 @@ impl NativeInterface for VirtualMachineRuntime { glyph: Glyph { glyph_id, palette_id }, x, y, + layer: 0, bank_id, active, flip_x, -- 2.47.2 From 3931e86b4197dabd3b8f164476aad6c4a3102394 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 13:25:40 +0100 Subject: [PATCH 06/21] implements PLN-0019 --- .../prometeu-drivers/src/frame_composer.rs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index f5b83c8e..63930e94 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -22,6 +22,7 @@ const EMPTY_SPRITE: Sprite = Sprite { pub enum SceneStatus { #[default] Unbound, + Available { scene_bank_id: usize }, } #[derive(Clone, Debug)] @@ -177,6 +178,35 @@ impl FrameComposer { (self.camera_x_px, self.camera_y_px) } + pub fn bind_scene(&mut self, scene_bank_id: usize) -> bool { + let Some(scene) = self.scene_bank_pool.scene_bank_slot(scene_bank_id) else { + self.unbind_scene(); + return false; + }; + + let (cache, resolver) = + Self::build_scene_runtime(self.viewport_width_px, self.viewport_height_px, &scene); + self.active_scene_id = Some(scene_bank_id); + self.active_scene = Some(scene); + self.scene_status = SceneStatus::Available { scene_bank_id }; + self.cache = Some(cache); + self.resolver = Some(resolver); + true + } + + pub fn unbind_scene(&mut self) { + self.active_scene_id = None; + self.active_scene = None; + self.scene_status = SceneStatus::Unbound; + self.cache = None; + self.resolver = None; + } + + pub fn set_camera(&mut self, x: i32, y: i32) { + self.camera_x_px = x; + self.camera_y_px = y; + } + pub fn cache(&self) -> Option<&SceneViewportCache> { self.cache.as_ref() } @@ -204,6 +234,31 @@ impl FrameComposer { pub fn ordered_sprites(&self) -> Vec { self.sprite_controller.ordered_sprites() } + + fn build_scene_runtime( + viewport_width_px: usize, + viewport_height_px: usize, + scene: &SceneBank, + ) -> (SceneViewportCache, SceneViewportResolver) { + let min_tile_px = + scene.layers.iter().map(|layer| layer.tile_size as usize).min().unwrap_or(8); + let cache_width_tiles = viewport_width_px.div_ceil(min_tile_px) + 5; + let cache_height_tiles = viewport_height_px.div_ceil(min_tile_px) + 4; + let hysteresis_safe_px = min_tile_px.saturating_sub(4) as i32; + let hysteresis_trigger_px = (min_tile_px + 4) as i32; + + ( + SceneViewportCache::new(scene, cache_width_tiles, cache_height_tiles), + SceneViewportResolver::new( + viewport_width_px as i32, + viewport_height_px as i32, + cache_width_tiles, + cache_height_tiles, + hysteresis_safe_px, + hysteresis_trigger_px, + ), + ) + } } #[cfg(test)] @@ -269,6 +324,75 @@ mod tests { assert_eq!(scene.layers[0].parallax_factor.y, 0.5); } + #[test] + fn bind_scene_stores_scene_identity_and_shared_reference() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_scene_bank(3, Arc::new(make_scene())); + + let expected_scene = banks.scene_bank_slot(3).expect("scene bank slot 3 should be resident"); + let mut frame_composer = FrameComposer::new(320, 180, banks); + + assert!(frame_composer.bind_scene(3)); + + assert_eq!(frame_composer.active_scene_id(), Some(3)); + assert!(Arc::ptr_eq( + frame_composer.active_scene().expect("active scene should exist"), + &expected_scene, + )); + assert_eq!(frame_composer.scene_status(), SceneStatus::Available { scene_bank_id: 3 }); + assert!(frame_composer.cache().is_some()); + assert!(frame_composer.resolver().is_some()); + } + + #[test] + fn unbind_scene_clears_scene_and_cache_state() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_scene_bank(1, Arc::new(make_scene())); + + let mut frame_composer = FrameComposer::new(320, 180, banks); + assert!(frame_composer.bind_scene(1)); + + frame_composer.unbind_scene(); + + assert_eq!(frame_composer.active_scene_id(), None); + assert!(frame_composer.active_scene().is_none()); + assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound); + assert!(frame_composer.cache().is_none()); + assert!(frame_composer.resolver().is_none()); + } + + #[test] + fn set_camera_stores_top_left_pixel_coordinates() { + let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + + frame_composer.set_camera(-12, 48); + + assert_eq!(frame_composer.camera(), (-12, 48)); + } + + #[test] + fn bind_scene_derives_cache_and_resolver_from_eight_pixel_layers() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_scene_bank(0, Arc::new(make_scene())); + + let mut frame_composer = FrameComposer::new(320, 180, banks); + assert!(frame_composer.bind_scene(0)); + + let cache = frame_composer.cache().expect("cache should exist for bound scene"); + assert_eq!((cache.width(), cache.height()), (45, 27)); + } + + #[test] + fn missing_scene_binding_falls_back_to_no_scene_state() { + let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + + assert!(!frame_composer.bind_scene(7)); + + assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound); + assert!(frame_composer.cache().is_none()); + assert!(frame_composer.resolver().is_none()); + } + #[test] fn sprite_controller_begin_frame_resets_sprite_count_and_buckets() { let mut controller = SpriteController::new(); -- 2.47.2 From 5ef43045bcad001e8585aaefed85fd4431cfbcbb Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 13:28:34 +0100 Subject: [PATCH 07/21] implements PLN-0020 --- .../prometeu-drivers/src/frame_composer.rs | 183 +++++++++++++++++- 1 file changed, 175 insertions(+), 8 deletions(-) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index 63930e94..378423c4 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -1,8 +1,9 @@ use crate::memory_banks::SceneBankPoolAccess; +use prometeu_hal::GfxBridge; use prometeu_hal::glyph::Glyph; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::scene_viewport_cache::SceneViewportCache; -use prometeu_hal::scene_viewport_resolver::SceneViewportResolver; +use prometeu_hal::scene_viewport_resolver::{CacheRefreshRequest, SceneViewportResolver}; use prometeu_hal::sprite::Sprite; use std::sync::Arc; @@ -22,7 +23,9 @@ const EMPTY_SPRITE: Sprite = Sprite { pub enum SceneStatus { #[default] Unbound, - Available { scene_bank_id: usize }, + Available { + scene_bank_id: usize, + }, } #[derive(Clone, Debug)] @@ -235,6 +238,22 @@ impl FrameComposer { self.sprite_controller.ordered_sprites() } + pub fn render_frame(&mut self, gfx: &mut dyn GfxBridge) { + let ordered_sprites = self.ordered_sprites(); + gfx.load_frame_sprites(&ordered_sprites); + + if let (Some(scene), Some(cache), Some(resolver)) = + (self.active_scene.as_deref(), self.cache.as_mut(), self.resolver.as_mut()) + { + let update = resolver.update(scene, self.camera_x_px, self.camera_y_px); + Self::apply_refresh_requests(cache, scene, &update.refresh_requests); + gfx.render_scene_from_cache(cache, &update); + return; + } + + gfx.render_all(); + } + fn build_scene_runtime( viewport_width_px: usize, viewport_height_px: usize, @@ -259,22 +278,57 @@ impl FrameComposer { ), ) } + + fn apply_refresh_requests( + cache: &mut SceneViewportCache, + scene: &SceneBank, + refresh_requests: &[CacheRefreshRequest], + ) { + for request in refresh_requests { + match *request { + CacheRefreshRequest::InvalidateLayer { layer_index } => { + cache.layers[layer_index].invalidate_all(); + } + CacheRefreshRequest::RefreshLine { layer_index, cache_y } => { + cache.refresh_layer_line(scene, layer_index, cache_y); + } + CacheRefreshRequest::RefreshColumn { layer_index, cache_x } => { + cache.refresh_layer_column(scene, layer_index, cache_x); + } + CacheRefreshRequest::RefreshRegion { layer_index, region } => { + cache.refresh_layer_region(scene, layer_index, region); + } + } + } + } } #[cfg(test)] mod tests { use super::*; - use crate::memory_banks::{MemoryBanks, SceneBankPoolInstaller}; - use prometeu_hal::glyph_bank::TileSize; + use crate::gfx::Gfx; + use crate::memory_banks::{ + GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller, + }; + use prometeu_hal::color::Color; + use prometeu_hal::glyph_bank::{GlyphBank, TileSize}; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; fn make_scene() -> SceneBank { + make_scene_with_palette(1, 1, TileSize::Size8) + } + + fn make_scene_with_palette( + glyph_bank_id: u8, + palette_id: u8, + tile_size: TileSize, + ) -> SceneBank { let layer = SceneLayer { active: true, - glyph_bank_id: 1, - tile_size: TileSize::Size8, + glyph_bank_id, + tile_size, parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 }, tilemap: TileMap { width: 2, @@ -282,7 +336,7 @@ mod tests { tiles: vec![ Tile { active: true, - glyph: Glyph { glyph_id: 9, palette_id: 1 }, + glyph: Glyph { glyph_id: 0, palette_id }, flip_x: false, flip_y: false, }; @@ -294,6 +348,16 @@ mod tests { SceneBank { layers: std::array::from_fn(|_| layer.clone()) } } + fn make_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank { + let size = tile_size as usize; + let mut bank = GlyphBank::new(tile_size, size, size); + bank.palettes[palette_id as usize][1] = color; + for pixel in &mut bank.pixel_indices { + *pixel = 1; + } + bank + } + #[test] fn frame_composer_starts_unbound_with_empty_owned_state() { let frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); @@ -329,7 +393,8 @@ mod tests { let banks = Arc::new(MemoryBanks::new()); banks.install_scene_bank(3, Arc::new(make_scene())); - let expected_scene = banks.scene_bank_slot(3).expect("scene bank slot 3 should be resident"); + let expected_scene = + banks.scene_bank_slot(3).expect("scene bank slot 3 should be resident"); let mut frame_composer = FrameComposer::new(320, 180, banks); assert!(frame_composer.bind_scene(3)); @@ -544,4 +609,106 @@ mod tests { assert_eq!(ordered[0].glyph.glyph_id, 20); assert_eq!(ordered[1].glyph.glyph_id, 21); } + + #[test] + fn render_frame_without_scene_uses_sprite_only_path() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE))); + + let mut frame_composer = + FrameComposer::new(16, 16, Arc::clone(&banks) as Arc); + frame_composer.begin_frame(); + assert!(frame_composer.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 0, palette_id: 3 }, + x: 0, + y: 0, + layer: 0, + bank_id: 1, + active: false, + flip_x: false, + flip_y: false, + priority: 0, + })); + + let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); + gfx.scene_fade_level = 31; + gfx.hud_fade_level = 31; + + frame_composer.render_frame(&mut gfx); + gfx.present(); + + assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw()); + } + + #[test] + fn render_frame_with_scene_applies_refreshes_before_composition() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE))); + banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 2, TileSize::Size8))); + + let mut frame_composer = + FrameComposer::new(16, 16, Arc::clone(&banks) as Arc); + assert!(frame_composer.bind_scene(0)); + + let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); + gfx.scene_fade_level = 31; + gfx.hud_fade_level = 31; + + frame_composer.render_frame(&mut gfx); + gfx.present(); + + assert!( + frame_composer + .cache() + .expect("cache should exist") + .layers + .iter() + .all(|layer| layer.valid) + ); + assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw()); + } + + #[test] + fn render_frame_survives_scene_transition_through_unbind_and_rebind() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 1, Color::RED))); + banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE))); + banks.install_glyph_bank(2, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE))); + banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 1, TileSize::Size8))); + banks.install_scene_bank(1, Arc::new(make_scene_with_palette(1, 2, TileSize::Size8))); + + let mut frame_composer = + FrameComposer::new(16, 16, Arc::clone(&banks) as Arc); + let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); + gfx.scene_fade_level = 31; + gfx.hud_fade_level = 31; + + assert!(frame_composer.bind_scene(0)); + frame_composer.render_frame(&mut gfx); + gfx.present(); + assert_eq!(gfx.front_buffer()[0], Color::RED.raw()); + + frame_composer.unbind_scene(); + frame_composer.begin_frame(); + assert!(frame_composer.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 0, palette_id: 3 }, + x: 0, + y: 0, + layer: 0, + bank_id: 2, + active: false, + flip_x: false, + flip_y: false, + priority: 0, + })); + frame_composer.render_frame(&mut gfx); + gfx.present(); + assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw()); + + frame_composer.begin_frame(); + assert!(frame_composer.bind_scene(1)); + frame_composer.render_frame(&mut gfx); + gfx.present(); + assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw()); + } } -- 2.47.2 From a1bd60671bb5d20370bcb6a093d9b9d4b9ad8513 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 13:32:11 +0100 Subject: [PATCH 08/21] implements PLN-0021 --- .../prometeu-drivers/src/frame_composer.rs | 2 +- crates/console/prometeu-drivers/src/gfx.rs | 6 +- .../console/prometeu-drivers/src/hardware.rs | 13 ++++ crates/console/prometeu-hal/src/gfx_bridge.rs | 2 +- .../prometeu-hal/src/hardware_bridge.rs | 5 ++ .../src/virtual_machine_runtime/dispatch.rs | 24 ++++--- .../src/virtual_machine_runtime/tests.rs | 67 ++++++++++++++++++- .../src/virtual_machine_runtime/tick.rs | 3 +- 8 files changed, 104 insertions(+), 18 deletions(-) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index 378423c4..9a174e18 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -251,7 +251,7 @@ impl FrameComposer { return; } - gfx.render_all(); + gfx.render_no_scene_frame(); } fn build_scene_runtime( diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index e78b3e4c..4a1c9dbb 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -209,8 +209,8 @@ impl GfxBridge for Gfx { fn present(&mut self) { self.present() } - fn render_all(&mut self) { - self.render_all() + fn render_no_scene_frame(&mut self) { + self.render_no_scene_frame() } fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) { self.render_scene_from_cache(cache, update) @@ -552,7 +552,7 @@ impl Gfx { /// This method composes the final frame by rasterizing layers and sprites in the /// correct priority order into the back buffer. /// Follows the hardware model where layers and sprites are composed every frame. - pub fn render_all(&mut self) { + pub fn render_no_scene_frame(&mut self) { self.populate_layer_buckets(); for bucket in &self.layer_buckets { Self::draw_bucket_on_buffer( diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index e1ef2d3f..c9de3447 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -9,6 +9,7 @@ use crate::memory_banks::{ use crate::pad::Pad; use crate::touch::Touch; use prometeu_hal::cartridge::AssetsPayloadSource; +use prometeu_hal::sprite::Sprite; use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge}; use std::sync::Arc; @@ -46,6 +47,18 @@ impl Default for Hardware { } impl HardwareBridge for Hardware { + fn begin_frame(&mut self) { + self.frame_composer.begin_frame(); + } + + fn emit_sprite(&mut self, sprite: Sprite) { + let _ = self.frame_composer.emit_sprite(sprite); + } + + fn render_frame(&mut self) { + self.frame_composer.render_frame(&mut self.gfx); + } + fn gfx(&self) -> &dyn GfxBridge { &self.gfx } diff --git a/crates/console/prometeu-hal/src/gfx_bridge.rs b/crates/console/prometeu-hal/src/gfx_bridge.rs index b000d20d..44829579 100644 --- a/crates/console/prometeu-hal/src/gfx_bridge.rs +++ b/crates/console/prometeu-hal/src/gfx_bridge.rs @@ -48,7 +48,7 @@ pub trait GfxBridge { fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color); fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color); fn present(&mut self); - fn render_all(&mut self); + fn render_no_scene_frame(&mut self); fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate); fn load_frame_sprites(&mut self, sprites: &[Sprite]); fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color); diff --git a/crates/console/prometeu-hal/src/hardware_bridge.rs b/crates/console/prometeu-hal/src/hardware_bridge.rs index 1e8d9728..2bcb3376 100644 --- a/crates/console/prometeu-hal/src/hardware_bridge.rs +++ b/crates/console/prometeu-hal/src/hardware_bridge.rs @@ -2,9 +2,14 @@ use crate::asset_bridge::AssetBridge; use crate::audio_bridge::AudioBridge; use crate::gfx_bridge::GfxBridge; use crate::pad_bridge::PadBridge; +use crate::sprite::Sprite; use crate::touch_bridge::TouchBridge; pub trait HardwareBridge { + fn begin_frame(&mut self); + fn emit_sprite(&mut self, sprite: Sprite); + fn render_frame(&mut self); + fn gfx(&self) -> &dyn GfxBridge; fn gfx_mut(&mut self) -> &mut dyn GfxBridge; diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs index 86184215..e3ec62ef 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -162,17 +162,19 @@ impl NativeInterface for VirtualMachineRuntime { return Ok(()); } - *hw.gfx_mut().sprite_mut(index) = Sprite { - glyph: Glyph { glyph_id, palette_id }, - x, - y, - layer: 0, - bank_id, - active, - flip_x, - flip_y, - priority, - }; + if active { + hw.emit_sprite(Sprite { + glyph: Glyph { glyph_id, palette_id }, + x, + y, + layer: 0, + bank_id, + active: false, + flip_x, + flip_y, + priority, + }); + } ret.push_int(GfxOpStatus::Ok as i64); Ok(()) } diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index 4c0c3b38..53df08e8 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -5,6 +5,7 @@ use prometeu_bytecode::Value; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl}; use prometeu_drivers::hardware::Hardware; +use prometeu_drivers::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller}; use prometeu_hal::AudioOpStatus; use prometeu_hal::GfxOpStatus; use prometeu_hal::InputSignals; @@ -12,10 +13,17 @@ use prometeu_hal::asset::{ AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus, }; use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge}; -use prometeu_hal::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1; +use prometeu_hal::color::Color; +use prometeu_hal::glyph::Glyph; +use prometeu_hal::glyph_bank::{GLYPH_BANK_PALETTE_COUNT_V1, GlyphBank, TileSize}; +use prometeu_hal::scene_bank::SceneBank; +use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::syscalls::caps; +use prometeu_hal::tile::Tile; +use prometeu_hal::tilemap::TileMap; use prometeu_vm::VmInitError; use std::collections::HashMap; +use std::sync::Arc; use std::sync::atomic::Ordering; #[derive(Default)] @@ -129,6 +137,40 @@ fn test_glyph_asset_data() -> Vec { data } +fn runtime_test_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank { + let size = tile_size as usize; + let mut bank = GlyphBank::new(tile_size, size, size); + bank.palettes[palette_id as usize][1] = color; + for pixel in &mut bank.pixel_indices { + *pixel = 1; + } + bank +} + +fn runtime_test_scene(glyph_bank_id: u8, palette_id: u8, tile_size: TileSize) -> SceneBank { + let layer = SceneLayer { + active: true, + glyph_bank_id, + tile_size, + parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, + tilemap: TileMap { + width: 2, + height: 2, + tiles: vec![ + Tile { + active: true, + glyph: Glyph { glyph_id: 0, palette_id }, + flip_x: false, + flip_y: false, + }; + 4 + ], + }, + }; + + SceneBank { layers: std::array::from_fn(|_| layer.clone()) } +} + #[test] fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { let mut runtime = VirtualMachineRuntime::new(None); @@ -233,6 +275,29 @@ fn tick_returns_panic_report_distinct_from_trap() { assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmPanic { .. }))); } +#[test] +fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let signals = InputSignals::default(); + let program = + serialized_single_function_module(assemble("FRAME_SYNC\nHALT").expect("assemble"), vec![]); + let cartridge = cartridge_with_program(program, caps::NONE); + + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE))); + banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8))); + let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks)); + assert!(hardware.frame_composer.bind_scene(0)); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick(&mut vm, &signals, &mut hardware); + + assert!(report.is_none(), "frame render path must not crash"); + hardware.gfx.present(); + assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); +} + #[test] fn initialize_vm_success_clears_previous_crash_report() { let mut runtime = VirtualMachineRuntime::new(None); diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs index ba58892b..4a281f66 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs @@ -145,7 +145,7 @@ impl VirtualMachineRuntime { if run.reason == LogicalFrameEndingReason::FrameSync || run.reason == LogicalFrameEndingReason::EndOfRom { - hw.gfx_mut().render_all(); + hw.render_frame(); // 1. Snapshot full telemetry at logical frame end let (glyph_bank, sound_bank) = Self::bank_telemetry_summary(hw); @@ -250,6 +250,7 @@ impl VirtualMachineRuntime { _signals: &InputSignals, hw: &mut dyn HardwareBridge, ) { + hw.begin_frame(); hw.audio_mut().clear_commands(); self.logs_written_this_frame.clear(); } -- 2.47.2 From f4260d0cf4bc77855b069fbfad4810b949f1c13b Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 16:36:12 +0100 Subject: [PATCH 09/21] adjustments over frame composer contract - agnostic tile size --- crates/console/prometeu-drivers/src/gfx.rs | 63 ++++---- .../prometeu-hal/src/scene_viewport_cache.rs | 135 ++++++++++++++++++ 2 files changed, 170 insertions(+), 28 deletions(-) diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 4a1c9dbb..47b06899 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -79,6 +79,21 @@ pub struct Gfx { const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7]; +struct RenderTarget<'a> { + back: &'a mut [u16], + screen_w: usize, + screen_h: usize, +} + +#[derive(Clone, Copy)] +struct CachedTileDraw<'a> { + x: i32, + y: i32, + entry: CachedTileEntry, + bank: &'a GlyphBank, + tile_size: prometeu_hal::glyph_bank::TileSize, +} + #[inline] fn glyph_for_char(c: char) -> &'static [u8; 5] { match c.to_ascii_uppercase() { @@ -629,6 +644,7 @@ impl Gfx { request: &LayerCopyRequest, glyph_banks: &dyn GlyphBankPoolAccess, ) { + let mut target = RenderTarget { back, screen_w, screen_h }; let layer_cache = &cache.layers[request.layer_index]; if !layer_cache.valid { return; @@ -657,52 +673,43 @@ impl Gfx { } Self::draw_cached_tile_pixels( - back, - screen_w, - screen_h, - screen_tile_x, - screen_tile_y, - entry, - &bank, - request.tile_size, + &mut target, + CachedTileDraw { + x: screen_tile_x, + y: screen_tile_y, + entry, + bank: &bank, + tile_size: request.tile_size, + }, ); } } } - fn draw_cached_tile_pixels( - back: &mut [u16], - screen_w: usize, - screen_h: usize, - x: i32, - y: i32, - entry: CachedTileEntry, - bank: &GlyphBank, - tile_size: prometeu_hal::glyph_bank::TileSize, - ) { - let size = tile_size as usize; + fn draw_cached_tile_pixels(target: &mut RenderTarget<'_>, tile: CachedTileDraw<'_>) { + let size = tile.tile_size as usize; for local_y in 0..size { - let world_y = y + local_y as i32; - if world_y < 0 || world_y >= screen_h as i32 { + let world_y = tile.y + local_y as i32; + if world_y < 0 || world_y >= target.screen_h as i32 { continue; } for local_x in 0..size { - let world_x = x + local_x as i32; - if world_x < 0 || world_x >= screen_w as i32 { + let world_x = tile.x + local_x as i32; + if world_x < 0 || world_x >= target.screen_w as i32 { continue; } - let fetch_x = if entry.flip_x() { size - 1 - local_x } else { local_x }; - let fetch_y = if entry.flip_y() { size - 1 - local_y } else { local_y }; - let px_index = bank.get_pixel_index(entry.glyph_id, fetch_x, fetch_y); + let fetch_x = if tile.entry.flip_x() { size - 1 - local_x } else { local_x }; + let fetch_y = if tile.entry.flip_y() { size - 1 - local_y } else { local_y }; + let px_index = tile.bank.get_pixel_index(tile.entry.glyph_id, fetch_x, fetch_y); if px_index == 0 { continue; } - let color = bank.resolve_color(entry.palette_id, px_index); - back[world_y as usize * screen_w + world_x as usize] = color.raw(); + let color = tile.bank.resolve_color(tile.entry.palette_id, px_index); + target.back[world_y as usize * target.screen_w + world_x as usize] = color.raw(); } } } diff --git a/crates/console/prometeu-hal/src/scene_viewport_cache.rs b/crates/console/prometeu-hal/src/scene_viewport_cache.rs index 4c2ee614..028cc7ff 100644 --- a/crates/console/prometeu-hal/src/scene_viewport_cache.rs +++ b/crates/console/prometeu-hal/src/scene_viewport_cache.rs @@ -325,6 +325,34 @@ mod tests { assert_eq!(cache.layers[0].ring_origin(), (1, 1)); } + #[test] + fn layer_cache_wraps_ring_origin_for_negative_and_large_movements() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 3, 3); + + cache.move_layer_window_by(0, -1, -4); + assert_eq!(cache.layers[0].logical_origin(), (-1, -4)); + assert_eq!(cache.layers[0].ring_origin(), (2, 2)); + + cache.move_layer_window_by(0, 7, 8); + assert_eq!(cache.layers[0].logical_origin(), (6, 4)); + assert_eq!(cache.layers[0].ring_origin(), (0, 1)); + } + + #[test] + fn move_window_to_matches_incremental_ring_movement() { + let scene = make_scene(); + let mut direct = SceneViewportCache::new(&scene, 4, 4); + let mut incremental = SceneViewportCache::new(&scene, 4, 4); + + direct.move_layer_window_to(0, 9, -6); + incremental.move_layer_window_by(0, 5, -2); + incremental.move_layer_window_by(0, 4, -4); + + assert_eq!(direct.layers[0].logical_origin(), incremental.layers[0].logical_origin()); + assert_eq!(direct.layers[0].ring_origin(), incremental.layers[0].ring_origin()); + } + #[test] fn cache_entry_fields_are_derived_from_scene_tiles() { let scene = make_scene(); @@ -415,6 +443,113 @@ mod tests { assert_eq!(cache.layers[3].entry(3, 3).glyph_id, 415); } + #[test] + fn refresh_after_wrapped_window_move_materializes_new_logical_tiles() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 3, 3); + + cache.refresh_layer_all(&scene, 0); + cache.move_layer_window_to(0, 1, 2); + cache.refresh_layer_all(&scene, 0); + + assert_eq!(cache.layers[0].logical_origin(), (1, 2)); + assert_eq!(cache.layers[0].ring_origin(), (1, 2)); + assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 109); + assert_eq!(cache.layers[0].entry(1, 0).glyph_id, 110); + assert_eq!(cache.layers[0].entry(2, 0).glyph_id, 111); + assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 113); + assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 115); + assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default()); + } + + #[test] + fn partial_refresh_uses_wrapped_physical_slots_after_window_move() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 3, 3); + + cache.move_layer_window_to(0, 1, 0); + cache.refresh_layer_column(&scene, 0, 0); + + assert_eq!(cache.layers[0].ring_origin(), (1, 0)); + assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 101); + assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 105); + assert_eq!(cache.layers[0].entry(0, 2).glyph_id, 109); + assert_eq!(cache.layers[0].entry(1, 0), CachedTileEntry::default()); + assert_eq!(cache.layers[0].entry(2, 0), CachedTileEntry::default()); + } + + #[test] + fn out_of_bounds_logical_origins_materialize_default_entries_after_wrap() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 2, 2); + + cache.move_layer_window_to(0, -2, 3); + cache.refresh_layer_all(&scene, 0); + + for y in 0..2 { + for x in 0..2 { + assert_eq!(cache.layers[0].entry(x, y), CachedTileEntry::default()); + } + } + } + + #[test] + fn ringbuffer_preserves_logical_tile_mapping_across_long_mixed_movements() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 3, 3); + let motions = [ + (1, 0), + (0, 1), + (2, 2), + (-1, 0), + (0, -2), + (4, 1), + (-3, 3), + (5, -4), + (-6, 2), + (3, -3), + (7, 7), + (-8, -5), + ]; + + for &(dx, dy) in &motions { + cache.move_layer_window_by(0, dx, dy); + cache.refresh_layer_all(&scene, 0); + + let (origin_x, origin_y) = cache.layers[0].logical_origin(); + for cache_y in 0..cache.height() { + for cache_x in 0..cache.width() { + let expected_scene_x = origin_x + cache_x as i32; + let expected_scene_y = origin_y + cache_y as i32; + + let expected = if expected_scene_x < 0 + || expected_scene_y < 0 + || expected_scene_x as usize >= scene.layers[0].tilemap.width + || expected_scene_y as usize >= scene.layers[0].tilemap.height + { + CachedTileEntry::default() + } else { + let tile_x = expected_scene_x as usize; + let tile_y = expected_scene_y as usize; + let tile = scene.layers[0].tilemap.tiles + [tile_y * scene.layers[0].tilemap.width + tile_x]; + CachedTileEntry::from_tile(&scene.layers[0], tile) + }; + + assert_eq!( + cache.layers[0].entry(cache_x, cache_y), + expected, + "mismatch at logical origin ({}, {}), cache ({}, {})", + origin_x, + origin_y, + cache_x, + cache_y + ); + } + } + } + } + #[test] fn materialization_populates_all_four_layers() { let scene = make_scene(); -- 2.47.2 From 240fe65da758859f5410265e4f5067aacb9b44f2 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 17:43:25 +0100 Subject: [PATCH 10/21] frame composer - abi adjustments --- discussion/index.ndjson | 3 +- ...7-frame-composer-public-syscall-surface.md | 214 ++++++++++++++++++ ...5-frame-composer-public-syscall-surface.md | 166 ++++++++++++++ ...ser-syscall-domain-and-spec-propagation.md | 122 ++++++++++ ...ser-runtime-dispatch-and-legacy-removal.md | 112 +++++++++ ...tridge-tooling-and-regression-migration.md | 107 +++++++++ ...PLN-0025-final-ci-validation-and-polish.md | 96 ++++++++ 7 files changed, 819 insertions(+), 1 deletion(-) create mode 100644 discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md create mode 100644 discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md create mode 100644 discussion/workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md create mode 100644 discussion/workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md create mode 100644 discussion/workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md create mode 100644 discussion/workflow/plans/PLN-0025-final-ci-validation-and-polish.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index b8501bd6..c8617971 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":27,"AGD":27,"DEC":15,"PLN":22,"LSN":31,"CLSN":1}} +{"type":"meta","next_id":{"DSC":28,"AGD":28,"DEC":16,"PLN":26,"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"}]} @@ -19,6 +19,7 @@ {"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-15","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-15"}],"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-15","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-15","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-15","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-15","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-15","ref_decisions":["DEC-0014"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"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"}]} diff --git a/discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md b/discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md new file mode 100644 index 00000000..560720cd --- /dev/null +++ b/discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md @@ -0,0 +1,214 @@ +--- +id: AGD-0027 +ticket: frame-composer-public-syscall-surface +title: Agenda - FrameComposer Public Syscall Surface +status: accepted +created: 2026-04-17 +updated: 2026-04-17 +tags: [gfx, runtime, syscall, abi, frame-composer, scene, camera, sprites] +--- + +## Contexto + +`DEC-0014` e os planos `PLN-0017` a `PLN-0021` fecharam a migração interna do pipeline de frame para `FrameComposer`: + +- `FrameComposer` virou o orquestrador canônico do frame; +- `Hardware` passou a agregá-lo ao lado de `Gfx`; +- scene, camera, cache, resolver e sprite emission migraram para ownership interno dele; +- o frame loop do runtime passou a renderizar via `FrameComposer.render_frame()`. + +Isso resolveu a base operacional interna, mas não fechou a superfície pública equivalente para a VM. A ABI pública ainda expõe apenas o contrato legado de `gfx.set_sprite(...)`, enquanto `bind_scene(...)` e `set_camera(...)` existem apenas como APIs internas do driver. + +Na prática, hoje temos uma assimetria: + +- a base canônica do frame está em `FrameComposer`; +- mas a ABI pública ainda não trata `FrameComposer` como serviço canônico para scene, camera e sprites. + +Essa lacuna impede a migração do restante da stack e também impede um stress cartridge que atravesse de verdade o pipeline novo por syscall pública. + +## Problema + +Precisamos definir a nova superfície pública de syscall para o pipeline canônico de `FrameComposer` sem reabrir a decisão já aceita sobre ownership interno do frame. + +O problema concreto não é “adicionar 2 ou 3 syscalls”. Precisamos decidir: + +- quais operações de `FrameComposer` viram ABI pública agora; +- se `gfx.set_sprite(...)` continua como shim legado ou perde status canônico; +- qual é o contrato mínimo de scene/camera que a VM pode observar/controlar; +- como nomear e versionar essa superfície pública sem criar um segundo modelo canônico concorrente; +- qual é a estratégia de transição para cartridge, runtime tests e stress tests; +- como propagar essa mudança para a spec canônica e, se necessário, para contratos de ABI e `ISA_CORE`. + +## Pontos Criticos + +- `DEC-0014` já fechou `FrameComposer` como base canônica interna; esta agenda não deve reabrir isso. +- A ABI pública atual ainda expõe `gfx.set_sprite(...)` com semântica herdada de índice/slot, mesmo que a implementação interna já use frame emission. +- `bind_scene(scene_bank_id)` e `set_camera(x, y)` já existem no driver, mas ainda não existem como syscalls públicas. +- Se a nova ABI expuser demais logo de início, vamos congelar cedo demais detalhes que ainda não provaram valor operacional. +- Se a nova ABI expuser de menos, manteremos um modelo híbrido por tempo demais: + - canônico internamente via `FrameComposer`; + - legado externamente via `Gfx`/`set_sprite`. +- Precisamos decidir se o namespace público continua em `gfx.*` por estabilidade do domínio, ou se devemos introduzir algo como `frame.*`. +- A transição precisa preservar compatibilidade suficiente para não quebrar cartridges e testes existentes antes da migração do restante. +- O contrato de sprite precisa deixar claro se o chamador ainda informa índice, se informa `layer`, e se `active` continua existindo na superfície pública. +- A mudança não pode ficar só em código/runtime; a spec canônica precisa ser atualizada para refletir o novo serviço público. +- Se o contrato público afetar superfícies documentadas de ABI ou o material de `ISA_CORE`, essa propagação precisa ser tratada como parte da mesma thread, não como follow-up solto. + +## Opcoes + +### Opcao 1 - Expor um núcleo mínimo canônico em `gfx.*` + +**Como seria:** +Adicionar apenas a superfície mínima para a VM controlar o pipeline novo: + +- `gfx.bind_scene(bank_id)` +- `gfx.unbind_scene()` +- `gfx.set_camera(x, y)` +- `gfx.emit_sprite(...)` + +`gfx.set_sprite(...)` permaneceria por um período como shim legado de compatibilidade. + +**Vantagens:** +- fecha rapidamente a lacuna operacional; +- habilita stress real do pipeline novo; +- reduz o tempo de convivência entre modelo canônico e legado; +- mantém o domínio público em `gfx`, evitando churn de namespace. + +**Desvantagens:** +- introduz ABI nova que precisará de migração coordenada; +- exige definir `emit_sprite(...)` com cuidado para não herdar sem querer o modelo de slot. + +### Opcao 2 - Expor scene/camera agora e adiar o contrato novo de sprite + +**Como seria:** +Publicar apenas: + +- `gfx.bind_scene(bank_id)` +- `gfx.unbind_scene()` +- `gfx.set_camera(x, y)` + +Sprites continuariam publicamente via `gfx.set_sprite(...)` até uma segunda fase. + +**Vantagens:** +- menor mudança imediata de ABI; +- desbloqueia o stress do world path e da câmera; +- reduz o volume inicial da migração pública. + +**Desvantagens:** +- mantém dois modelos públicos de sprite por mais tempo; +- prolonga a semântica de compatibilidade do syscall legado; +- adia exatamente uma das partes centrais da migração para `FrameComposer`. + +### Opcao 3 - Criar um novo namespace público separado, como `composer.*` + +**Como seria:** +O pipeline novo ganha syscalls em um domínio separado, por exemplo: + +- `composer.bind_scene` +- `composer.unbind_scene` +- `composer.set_camera` +- `composer.emit_sprite` + +`gfx.*` ficaria como superfície legacy/low-level. + +**Vantagens:** +- deixa explícita a mudança de serviço canônico; +- evita sobrecarregar semanticamente `gfx`. + +**Desvantagens:** +- adiciona churn conceitual e de nomenclatura; +- fragmenta demais a superfície pública neste momento; +- cria um custo de transição maior sem benefício operacional evidente. + +## Sugestao / Recomendacao + +Seguir com a **Opcao 3**. + +Direção recomendada: + +- a superfície pública canônica deve migrar para o domínio `composer.*`; +- `FrameComposer` vira a base canônica também na ABI pública, com namespace próprio em vez de continuar semanticamente preso a `gfx.*`; +- o núcleo mínimo público deve ser: + - `composer.bind_scene(bank_id) -> status` + - `composer.unbind_scene()` + - `composer.set_camera(x, y)` + - `composer.emit_sprite(...) -> status` +- `gfx.set_sprite(...)` deve morrer e ser removido completamente do contrato público. + +Para sprites, a recomendação provisória é: + +- a nova ABI pública não deve exigir índice explícito; +- `composer.emit_sprite(...)` deve receber o payload completo necessário para o frame: + - `glyph_id` + - `palette_id` + - `x` + - `y` + - `layer` + - `bank_id` + - `flip_x` + - `flip_y` + - `priority` +- a ABI pode futuramente agrupar esse payload se isso melhorar ergonomia, mas o contrato mínimo deve nascer completo; +- `active` não deve continuar no contrato canônico novo; +- overflow continua sendo ignorado com status/telemetria adequada, sem trapar o runtime. + +Para scene/camera, a recomendação provisória é: + +- manter o contrato mínimo já aceito internamente; +- `bind_scene` por bank id; +- `unbind_scene` explícito; +- `set_camera(x, y)` em pixel space com top-left viewport. +- `bind_scene(...)`, `unbind_scene(...)` e `emit_sprite(...)` devem usar `ComposerOpStatus` como retorno operacional canônico. + +## Perguntas em Aberto + +- Resolvido: + - o nome público canônico de sprite será `composer.emit_sprite(...)`; + - o syscall novo de sprite nasce completo com `glyph_id`, `palette_id`, `x`, `y`, `layer`, `bank_id`, `flip_x`, `flip_y`, `priority`; + - `gfx.set_sprite(...)` deve morrer e ser removido completamente; + - não haverá leitura de estado nesta primeira fase; + - `bind_scene(...)`, `unbind_scene(...)` e `emit_sprite(...)` usarão `ComposerOpStatus`; +- A ABI nova precisa expor refresh explícito, ou isso deve continuar totalmente interno ao `FrameComposer`? +- Resolvido: + - a ABI nova não deve expor refresh explícito; + - o domínio público canônico será `composer.*`, não `gfx.*`. + +## Criterio para Encerrar + +Esta agenda pode ser encerrada quando houver acordo explícito sobre: + +- a lista mínima de syscalls públicas canônicas do `FrameComposer`; +- o nome canônico da operação pública de sprite; +- a remoção completa de `gfx.set_sprite(...)` do contrato público; +- o formato de retorno/status das novas operações; +- a estratégia de transição necessária para decisão, plano e migração do restante da stack. + +## Resolucao em Andamento + +Direção atualmente acordada nesta agenda: + +- o namespace público canônico será `composer.*`; +- o núcleo mínimo inicial será: + - `composer.bind_scene(bank_id) -> ComposerOpStatus` + - `composer.unbind_scene() -> ComposerOpStatus` + - `composer.set_camera(x, y)` + - `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> ComposerOpStatus` +- não haverá introspecção pública nesta primeira fase; +- refresh/cache policy continua interno ao `FrameComposer`; +- `gfx.set_sprite(...)` não terá caminho de compatibilidade e deve ser removido. + +## Resolucao + +Esta agenda fica aceita com os seguintes pontos fechados: + +- o namespace público canônico do serviço será `composer.*`; +- a superfície mínima inicial será: + - `composer.bind_scene(bank_id) -> ComposerOpStatus` + - `composer.unbind_scene() -> ComposerOpStatus` + - `composer.set_camera(x, y)` + - `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> ComposerOpStatus` +- não haverá introspecção pública nesta primeira fase; +- não haverá refresh/cache policy público; +- `gfx.set_sprite(...)` deve ser removido completamente, sem shim de compatibilidade; +- a transição deve introduzir `composer.*` e remover `gfx.set_sprite(...)` na mesma thread de migração, com atualização coordenada de bytecode, cartridges, tests e runtime; +- a mesma thread deve atualizar a spec canônica do assunto e propagar a mudança para contratos de ABI e `ISA_CORE` quando essas superfícies forem impactadas pelo novo serviço público. diff --git a/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md b/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md new file mode 100644 index 00000000..f0fb8fba --- /dev/null +++ b/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md @@ -0,0 +1,166 @@ +--- +id: DEC-0015 +ticket: frame-composer-public-syscall-surface +title: FrameComposer Public Syscall Surface +status: accepted +created: 2026-04-17 +accepted: 2026-04-17 +agenda: AGD-0027 +plans: [PLN-0022, PLN-0023, PLN-0024, PLN-0025] +tags: [gfx, runtime, syscall, abi, frame-composer, scene, camera, sprites] +--- + +## Status + +Accepted. + +## Contexto + +`DEC-0014` locked `FrameComposer` as the canonical internal frame orchestration service and `PLN-0017` through `PLN-0021` completed that internal migration path. `Hardware` now owns `FrameComposer`, the runtime renders through `FrameComposer.render_frame()`, and scene/camera/cache/resolver/sprite ownership no longer belongs canonically to `Gfx`. + +That migration did not define the equivalent public syscall contract for VM code. The public ABI still exposed legacy `gfx`-domain sprite control while the canonical scene/camera operations existed only as internal driver APIs. + +This decision closes that public ABI gap without reopening the already accepted internal ownership model. + +## Decisao + +The canonical public syscall surface for frame orchestration SHALL move to the `composer.*` namespace. + +Normatively: + +- The canonical public service domain for `FrameComposer` operations SHALL be `composer`. +- The initial canonical syscall set SHALL be: + - `composer.bind_scene(bank_id) -> ComposerOpStatus` + - `composer.unbind_scene() -> ComposerOpStatus` + - `composer.set_camera(x, y)` + - `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> ComposerOpStatus` +- `composer.emit_sprite(...)` SHALL be the canonical public sprite submission path. +- `composer.emit_sprite(...)` MUST NOT require a caller-provided sprite index. +- `composer.emit_sprite(...)` MUST carry `layer` and `priority`. +- `composer.emit_sprite(...)` MUST NOT expose `active` as part of the canonical contract. +- `composer.bind_scene(...)`, `composer.unbind_scene()`, and `composer.emit_sprite(...)` SHALL return `ComposerOpStatus`. +- `composer.set_camera(x, y)` SHALL keep the minimal V1 camera contract already accepted by `DEC-0014`: + - `x` and `y` are `i32` pixel coordinates; + - they represent the top-left viewport origin in world space. +- The public ABI MUST NOT expose cache refresh policy or explicit refresh controls. +- The public ABI MUST NOT expose scene/camera introspection in this first phase. +- `gfx.set_sprite(...)` MUST be removed completely from the public contract. +- No compatibility shim for `gfx.set_sprite(...)` SHALL remain as part of the canonical migration target. +- Introduction of `composer.*` and removal of `gfx.set_sprite(...)` SHALL be executed in the same migration thread. + +## Rationale + +The public ABI must reflect the accepted ownership model rather than preserve a misleading legacy shape. + +Keeping the canonical public surface under `gfx.*` would continue to tie orchestration semantics to the wrong service boundary. The new namespace makes the ownership change explicit: + +- `Gfx` is the visual backend; +- `FrameComposer` is the frame orchestration service. + +Removing `gfx.set_sprite(...)` completely avoids prolonging a dual public sprite model. A compatibility shim would preserve legacy slot/index semantics in the public contract after those semantics had already ceased to be canonical internally. + +Returning `ComposerOpStatus` for operational mutating calls preserves status-first behavior while keeping the public contract aligned with the new service boundary. Reusing `GfxOpStatus` would leak backend-domain semantics into orchestration-domain syscalls after that separation had already been made explicit. + +Deferring introspection and explicit refresh controls keeps the first public ABI focused on control, not diagnostics or internal policy leakage. + +## Invariantes / Contrato + +### 1. Namespace + +- Public frame-orchestration syscalls MUST live under `composer.*`. +- `composer.*` SHALL be treated as the canonical public orchestration surface. +- `gfx.*` SHALL NOT remain the canonical public orchestration namespace for scene/camera/sprite submission. + +### 2. Scene Control + +- `composer.bind_scene(bank_id)` MUST bind by scene bank id. +- Binding semantics MUST remain aligned with `DEC-0014`: + - scene resolution through the scene bank pool; + - explicit bind/unbind lifecycle; + - no implicit per-frame rebinding. +- `composer.unbind_scene()` MUST leave no-scene rendering valid. +- `ComposerOpStatus` SHALL be the canonical operational status family for composer-domain mutating syscalls. + +### 3. Camera + +- `composer.set_camera(x, y)` MUST remain the minimal V1 camera API. +- Camera follow, smoothing, shake, transitions, and readback are OUT OF SCOPE for this decision. + +### 4. Sprite Submission + +- `composer.emit_sprite(...)` MUST be frame-emission based. +- The caller MUST NOT provide sprite slot/index information. +- The public payload MUST include: + - `glyph_id` + - `palette_id` + - `x` + - `y` + - `layer` + - `bank_id` + - `flip_x` + - `flip_y` + - `priority` +- The canonical public sprite contract MUST NOT include `active`. +- Overflow behavior SHALL remain aligned with `DEC-0014`: + - excess sprites are ignored; + - overflow is not a hard VM fault in V1. + +### 5. Non-Goals for V1 Public ABI + +- No public refresh/invalidate syscalls. +- No public cache inspection syscalls. +- No public `scene_status()` syscall. +- No public `get_camera()` syscall. + +### 6. Migration Contract + +- Migration MUST update: + - syscall registry and ABI resolution; + - runtime dispatch; + - bytecode/cartridge declarations; + - tests; + - stress cartridges and related tooling where applicable. +- Migration MUST NOT leave `gfx.set_sprite(...)` as a supported public fallback after the new contract lands. + +## Impactos + +### HAL + +- The syscall enum, registry, metadata, and resolver will need a new `composer` domain surface. +- `gfx.set_sprite(...)` must be removed from the public ABI contract. +- A new `ComposerOpStatus` contract will need to be introduced for composer-domain operational returns. + +### Runtime / VM + +- Runtime dispatch must route public scene/camera/sprite orchestration through `FrameComposer`. +- Existing bytecode declarations and cartridges that rely on `gfx.set_sprite(...)` will need coordinated migration. + +### Spec / ABI / ISA_CORE + +- The canonical spec for the public VM-facing graphics/composition surface must be updated to reflect `composer.*`. +- ABI-facing documentation and contracts must be updated wherever syscall domain, names, arguments, or return semantics are specified. +- `ISA_CORE` must be updated if and where it normatively references the public syscall surface affected by this decision. + +### Drivers / Hardware + +- `FrameComposer` already has the required internal base; execution work will focus on public ABI exposure rather than internal ownership redesign. + +### Tooling / Stress + +- Stress cartridges and bytecode generators can only exercise the canonical frame path publicly after `composer.*` exists. + +## Referencias + +- [AGD-0027-frame-composer-public-syscall-surface.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md) +- [DEC-0014-frame-composer-render-integration.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md) + +## Propagacao Necessaria + +- A new implementation plan MUST be created from this decision before code changes. +- The plan MUST cover ABI introduction, legacy syscall removal, cartridge/test migration, regression coverage, and canonical spec propagation. +- The plan MUST explicitly assess and update ABI and `ISA_CORE` artifacts where this decision changes documented public behavior. +- Stress tooling SHOULD be updated as part of the migration thread so the public ABI can exercise the canonical frame path end-to-end. + +## Revision Log + +- 2026-04-17: Initial accepted decision from `AGD-0027`. diff --git a/discussion/workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md b/discussion/workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md new file mode 100644 index 00000000..34916ad3 --- /dev/null +++ b/discussion/workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md @@ -0,0 +1,122 @@ +--- +id: PLN-0022 +ticket: frame-composer-public-syscall-surface +title: Plan - Composer Syscall Domain and Spec Propagation +status: accepted +created: 2026-04-17 +completed: +tags: [gfx, runtime, syscall, abi, spec, isa-core, frame-composer] +--- + +## Objective + +Introduce the canonical `composer.*` syscall domain, define `ComposerOpStatus`, and propagate the new public contract through the canonical spec, ABI documentation, and `ISA_CORE` artifacts where affected. + +## Background + +`DEC-0015` locks the public orchestration surface on `composer.*`, requires `ComposerOpStatus` for mutating composer-domain calls, and requires propagation beyond code into canonical spec, ABI-facing documentation, and `ISA_CORE` where the public syscall surface is described normatively. + +## Scope + +### Included +- add the `composer` syscall domain and ids +- define `ComposerOpStatus` +- remove `gfx.set_sprite(...)` from the public ABI contract +- update canonical spec documentation for the new public surface +- update ABI-facing documentation and `ISA_CORE` wherever the public syscall contract is described + +### Excluded +- runtime dispatch implementation +- cartridge and stress program migration +- final repository-wide CI execution + +## Execution Steps + +### Step 1 - Define the public `composer` syscall contract + +**What:** +Add the new canonical public syscall surface to the HAL syscall contract. + +**How:** +- Extend the syscall enum, registry, metadata, and resolver with a new `composer` domain. +- Allocate explicit syscall ids for: + - `composer.bind_scene` + - `composer.unbind_scene` + - `composer.set_camera` + - `composer.emit_sprite` +- Remove `gfx.set_sprite` from the public syscall contract and registry. +- Keep syscall metadata explicit for arg/ret slots and capability requirements. + +**File(s):** +- `crates/console/prometeu-hal/src/syscalls.rs` +- `crates/console/prometeu-hal/src/syscalls/domains/*` +- `crates/console/prometeu-hal/src/syscalls/registry.rs` +- `crates/console/prometeu-hal/src/syscalls/resolver.rs` + +### Step 2 - Introduce `ComposerOpStatus` + +**What:** +Create the status family for composer-domain mutating operations. + +**How:** +- Define a `ComposerOpStatus` type in HAL with explicit operational states needed by: + - scene binding + - scene unbinding + - sprite emission +- Ensure the enum is semantically composer-domain specific rather than a rename wrapper around `GfxOpStatus`. +- Update public API references so composer syscalls return `ComposerOpStatus` where required by `DEC-0015`. + +**File(s):** +- `crates/console/prometeu-hal/src/*` +- any shared status exports used by runtime/VM code + +### Step 3 - Propagate the contract into spec, ABI docs, and `ISA_CORE` + +**What:** +Update normative documentation so the public contract no longer describes legacy `gfx.set_sprite`. + +**How:** +- Identify canonical spec files that describe VM graphics/composition syscalls. +- Replace public references to legacy sprite orchestration with `composer.*`. +- Update ABI-facing docs to pin: + - namespace + - names + - arg order + - return semantics +- Update `ISA_CORE` if and where it references the affected syscall surface. +- Keep published spec content in English per repository policy. + +**File(s):** +- canonical spec location(s) +- ABI contract documentation +- `ISA_CORE` artifact(s) if affected + +## Test Requirements + +### Unit Tests +- syscall registry tests pin the new `composer.*` entries and reject removed legacy identities +- `ComposerOpStatus` values are pinned where public return semantics are asserted + +### Integration Tests +- declared syscall resolution accepts `composer.*` declarations and rejects removed `gfx.set_sprite` + +### Manual Verification +- inspect canonical spec, ABI docs, and `ISA_CORE` references to confirm the public contract matches `DEC-0015` + +## Acceptance Criteria + +- [ ] The public syscall registry exposes `composer.bind_scene`, `composer.unbind_scene`, `composer.set_camera`, and `composer.emit_sprite`. +- [ ] `ComposerOpStatus` exists as the canonical status family for composer-domain mutating syscalls. +- [ ] `gfx.set_sprite` is removed from the public ABI contract. +- [ ] Canonical spec documentation is updated to describe `composer.*`. +- [ ] ABI-facing docs and `ISA_CORE` are updated wherever the affected public surface is documented. + +## Dependencies + +- Source decision: `DEC-0015` + +## Risks + +- Missing a normative doc location would leave the code and published contract divergent. +- Reusing `GfxOpStatus` semantics by accident would weaken the service-boundary separation required by `DEC-0015`. +- Removing the legacy syscall contract incompletely could leave resolver or ABI ambiguity behind. diff --git a/discussion/workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md b/discussion/workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md new file mode 100644 index 00000000..70999ee8 --- /dev/null +++ b/discussion/workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md @@ -0,0 +1,112 @@ +--- +id: PLN-0023 +ticket: frame-composer-public-syscall-surface +title: Plan - Composer Runtime Dispatch and Legacy Removal +status: accepted +created: 2026-04-17 +completed: +tags: [runtime, syscall, frame-composer, dispatch, migration] +--- + +## Objective + +Route the new public `composer.*` syscalls through `FrameComposer`, remove legacy `gfx.set_sprite` handling, and align runtime-side operational behavior with `DEC-0015`. + +## Background + +`DEC-0015` closes the public contract around `composer.*` and requires that `gfx.set_sprite` be removed completely rather than kept as a compatibility shim. The internal `FrameComposer` ownership model already exists from `DEC-0014` and plans `PLN-0017` through `PLN-0021`. + +## Scope + +### Included +- runtime syscall dispatch for `composer.*` +- operational mapping from syscall args to `FrameComposer` +- removal of legacy `gfx.set_sprite` runtime handling +- runtime-facing tests for composer-domain behavior + +### Excluded +- spec and ABI doc propagation +- cartridge/tooling migration +- final `make ci` closure + +## Execution Steps + +### Step 1 - Add runtime dispatch for `composer.*` + +**What:** +Teach VM runtime dispatch to call `FrameComposer` through the new public contract. + +**How:** +- Add dispatch arms for: + - `composer.bind_scene` + - `composer.unbind_scene` + - `composer.set_camera` + - `composer.emit_sprite` +- Parse arguments exactly as pinned by the HAL metadata. +- Return `ComposerOpStatus` for mutating composer-domain syscalls. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` +- any adjacent runtime helpers + +### Step 2 - Map operational outcomes cleanly onto `ComposerOpStatus` + +**What:** +Make runtime failures and normal outcomes reflect the new composer-domain status model. + +**How:** +- Bind runtime-side operational checks to status outcomes such as: + - scene bank unavailable + - bank invalid + - argument range invalid + - layer invalid + - sprite overflow if surfaced operationally +- Keep non-fatal overflow behavior aligned with `DEC-0015`. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` +- `crates/console/prometeu-hal/src/*` as needed for shared status meaning + +### Step 3 - Remove legacy `gfx.set_sprite` runtime support + +**What:** +Delete the old public runtime path for slot-style sprite submission. + +**How:** +- Remove dispatch support for `gfx.set_sprite`. +- Remove runtime assumptions about `active`, caller-provided indices, and legacy sprite ABI shape. +- Keep no private compatibility hook behind the public API. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` +- adjacent tests and public syscall references + +## Test Requirements + +### Unit Tests +- runtime dispatch returns `ComposerOpStatus` for bind, unbind, and emit operations +- `composer.set_camera` stores the minimal V1 camera coordinates correctly + +### Integration Tests +- a VM/runtime test can bind a scene, set camera, emit a sprite, reach `FRAME_SYNC`, and render through the canonical frame path +- public runtime behavior rejects removed `gfx.set_sprite` declarations/calls + +### Manual Verification +- inspect dispatch code to confirm all public orchestration now routes through `FrameComposer` rather than a legacy `gfx` sprite syscall path + +## Acceptance Criteria + +- [ ] Runtime dispatch supports all canonical `composer.*` syscalls. +- [ ] Mutating composer-domain calls return `ComposerOpStatus`. +- [ ] `gfx.set_sprite` is removed from runtime public handling. +- [ ] Runtime tests cover scene bind, camera set, sprite emit, and frame rendering through the public path. + +## Dependencies + +- Depends on `PLN-0022` +- Source decision: `DEC-0015` + +## Risks + +- Removing legacy handling before all runtime references are migrated can strand tests or bytecode fixtures. +- Poor `ComposerOpStatus` mapping could collapse useful operational distinctions into generic failures. diff --git a/discussion/workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md b/discussion/workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md new file mode 100644 index 00000000..1e5fb3a1 --- /dev/null +++ b/discussion/workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md @@ -0,0 +1,107 @@ +--- +id: PLN-0024 +ticket: frame-composer-public-syscall-surface +title: Plan - Composer Cartridge, Tooling, and Regression Migration +status: accepted +created: 2026-04-17 +completed: +tags: [runtime, bytecode, tooling, stress, regression, frame-composer] +--- + +## Objective + +Migrate bytecode declarations, cartridges, stress tooling, and regression coverage from legacy public sprite orchestration to the canonical `composer.*` surface. + +## Background + +`DEC-0015` requires the new public composer-domain ABI to land without leaving `gfx.set_sprite` as a fallback. That means the migration must cover the generated bytecode, test cartridges, and stress tooling that still assume the old public contract. + +## Scope + +### Included +- bytecode declaration updates for `composer.*` +- cartridge and stress generator migration +- regression coverage for the public composer-domain path +- removal of legacy syscall usage from test and tooling surfaces + +### Excluded +- canonical spec propagation +- runtime dispatch implementation +- final repository-wide CI closure + +## Execution Steps + +### Step 1 - Migrate declared syscall users and fixtures + +**What:** +Update code and fixtures that declare public syscalls so they target `composer.*`. + +**How:** +- Replace legacy public sprite syscall declarations with composer-domain declarations. +- Update ABI expectations in bytecode-related tests and fixtures. +- Ensure removal of `gfx.set_sprite` is reflected in any declaration validation snapshots. + +**File(s):** +- bytecode tests and fixtures +- syscall declaration users across runtime and tools + +### Step 2 - Migrate stress and cartridge tooling + +**What:** +Make the stress cartridge and related generators exercise the canonical public frame path. + +**How:** +- Update `pbxgen-stress` and any cartridge generators to declare and call `composer.*`. +- Replace legacy sprite-path usage with `composer.emit_sprite`. +- Add scene bind and camera usage where needed so the stress path reaches the real canonical pipeline. + +**File(s):** +- `crates/tools/pbxgen-stress/src/*` +- `test-cartridges/stress-console/*` +- related scripts such as `scripts/run-stress.sh` + +### Step 3 - Expand regression coverage around the public path + +**What:** +Lock the new public orchestration contract with regression tests. + +**How:** +- Add tests that cover: + - composer-domain declaration resolution + - public bind/unbind/camera/emit behavior + - scene rendering through the public path + - stress/tooling integration using `composer.*` +- Ensure no regression fixture still relies on removed `gfx.set_sprite`. + +**File(s):** +- runtime tests +- HAL syscall tests +- tooling tests where available + +## Test Requirements + +### Unit Tests +- bytecode and syscall declaration tests pin `composer.*` names and slot counts + +### Integration Tests +- stress or cartridge-facing tests exercise scene bind, camera set, and sprite emit through `composer.*` +- regression fixtures fail if `gfx.set_sprite` is reintroduced + +### Manual Verification +- inspect generated stress cartridge declarations and program behavior to confirm the public path is truly composer-domain based + +## Acceptance Criteria + +- [ ] Bytecode declarations and fixtures use `composer.*` instead of legacy public sprite orchestration. +- [ ] Stress tooling and test cartridges exercise the canonical public `FrameComposer` path. +- [ ] Regression coverage protects against fallback to `gfx.set_sprite`. + +## Dependencies + +- Depends on `PLN-0022` and `PLN-0023` +- Source decision: `DEC-0015` + +## Risks + +- Partial cartridge/tooling migration could leave the repository with hidden legacy public ABI usage. +- Stress tooling may appear to pass while still missing scene/camera coverage if it only migrates sprite calls. diff --git a/discussion/workflow/plans/PLN-0025-final-ci-validation-and-polish.md b/discussion/workflow/plans/PLN-0025-final-ci-validation-and-polish.md new file mode 100644 index 00000000..fd510d0d --- /dev/null +++ b/discussion/workflow/plans/PLN-0025-final-ci-validation-and-polish.md @@ -0,0 +1,96 @@ +--- +id: PLN-0025 +ticket: frame-composer-public-syscall-surface +title: Plan - Final CI Validation and Polish +status: accepted +created: 2026-04-17 +completed: +tags: [ci, validation, regression, polish] +--- + +## Objective + +Run the final repository validation path, including `make ci`, and perform the last compatibility, formatting, lint, and regression fixes required to close the composer-domain migration cleanly. + +## Background + +`DEC-0015` requires a coordinated migration across ABI, runtime, tooling, cartridges, spec, and documentation. After the implementation plans land, the repository still needs a final closure pass so no residual breakage survives in formatting, linting, tests, generated artifacts, or CI expectations. + +## Scope + +### Included +- final repository validation with `make ci` +- fixups required by formatting, lint, tests, snapshots, or generated artifacts +- final consistency pass across migrated files + +### Excluded +- introducing new contract changes beyond `DEC-0015` +- reopening ABI or service-boundary decisions + +## Execution Steps + +### Step 1 - Run the final validation entrypoint + +**What:** +Execute the repository’s final CI validation path. + +**How:** +- Run `make ci` after `PLN-0022`, `PLN-0023`, and `PLN-0024` are complete. +- Capture failures from formatting, lint, tests, coverage setup, generation steps, or artifact drift. + +**File(s):** +- repository-wide validation entrypoints + +### Step 2 - Apply closure fixes without reopening scope + +**What:** +Resolve residual breakage surfaced by final validation. + +**How:** +- Fix formatting and lint issues. +- Update snapshots or generated artifacts only where the migrated public contract requires it. +- Repair any remaining tests or documentation references that fail under `make ci`. +- Do not widen scope beyond the accepted composer-domain migration. + +**File(s):** +- any files directly implicated by final validation failures + +### Step 3 - Confirm final repository consistency + +**What:** +Leave the migration in a stable publishable state. + +**How:** +- Re-run `make ci` until it passes cleanly. +- Verify no legacy public `gfx.set_sprite` usage remains in code, tests, tooling, or docs. +- Confirm the worktree reflects only intended migration changes. + +**File(s):** +- repository-wide + +## Test Requirements + +### Unit Tests +- whatever unit coverage is exercised by `make ci` must remain green + +### Integration Tests +- repository integration coverage under `make ci` must pass after the migration + +### Manual Verification +- inspect the tree for residual `gfx.set_sprite` references and incomplete composer-domain propagation + +## Acceptance Criteria + +- [ ] `make ci` passes after the composer-domain migration family lands. +- [ ] Final fixups do not reopen contract scope beyond `DEC-0015`. +- [ ] No residual public `gfx.set_sprite` usage remains in the repository. + +## Dependencies + +- Depends on `PLN-0022`, `PLN-0023`, and `PLN-0024` +- Source decision: `DEC-0015` + +## Risks + +- If this final closure pass is skipped, small residual regressions can survive across formatting, lint, or generated artifacts even when the core implementation is correct. +- Late fixes can accidentally widen scope unless kept strictly bounded to validation fallout. -- 2.47.2 From dd90ff812cfb6a6137c3924193c559e489af3e26 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 17:49:18 +0100 Subject: [PATCH 11/21] implements PLN-0022 --- .../prometeu-hal/src/composer_status.rs | 10 +++ crates/console/prometeu-hal/src/lib.rs | 2 + crates/console/prometeu-hal/src/syscalls.rs | 6 +- .../src/syscalls/domains/composer.rs | 22 ++++++ .../prometeu-hal/src/syscalls/domains/gfx.rs | 5 -- .../prometeu-hal/src/syscalls/domains/mod.rs | 2 + .../prometeu-hal/src/syscalls/registry.rs | 10 ++- .../prometeu-hal/src/syscalls/tests.rs | 50 ++++++++++--- .../src/virtual_machine_runtime/dispatch.rs | 55 ++------------ .../prometeu-vm/src/virtual_machine.rs | 33 ++++++--- docs/specs/runtime/04-gfx-peripheral.md | 73 ++++++++++--------- .../specs/runtime/16-host-abi-and-syscalls.md | 19 +++++ docs/vm-arch/ISA_CORE.md | 3 + 13 files changed, 179 insertions(+), 111 deletions(-) create mode 100644 crates/console/prometeu-hal/src/composer_status.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/composer.rs diff --git a/crates/console/prometeu-hal/src/composer_status.rs b/crates/console/prometeu-hal/src/composer_status.rs new file mode 100644 index 00000000..2e02b7ed --- /dev/null +++ b/crates/console/prometeu-hal/src/composer_status.rs @@ -0,0 +1,10 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(i32)] +pub enum ComposerOpStatus { + Ok = 0, + SceneUnavailable = 1, + ArgRangeInvalid = 2, + BankInvalid = 3, + LayerInvalid = 4, + SpriteOverflow = 5, +} diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index abfc7d7c..cc8de0a1 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -5,6 +5,7 @@ pub mod button; pub mod cartridge; pub mod cartridge_loader; pub mod color; +pub mod composer_status; pub mod debugger_protocol; pub mod gfx_bridge; pub mod glyph; @@ -34,6 +35,7 @@ pub mod window; pub use asset_bridge::AssetBridge; pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode}; +pub use composer_status::ComposerOpStatus; pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus}; pub use hardware_bridge::HardwareBridge; pub use host_context::{HostContext, HostContextProvider}; diff --git a/crates/console/prometeu-hal/src/syscalls.rs b/crates/console/prometeu-hal/src/syscalls.rs index c4a90381..3e9b269a 100644 --- a/crates/console/prometeu-hal/src/syscalls.rs +++ b/crates/console/prometeu-hal/src/syscalls.rs @@ -19,6 +19,7 @@ pub use resolver::{ /// Each Syscall has a unique 32-bit ID. The IDs are grouped by category: /// - **0x0xxx**: System & OS Control /// - **0x1xxx**: Graphics (GFX) +/// - **0x11xx**: Frame Composer orchestration /// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input) /// - **0x3xxx**: Audio (PCM & Mixing) /// - **0x4xxx**: Filesystem (Sandboxed I/O) @@ -35,9 +36,12 @@ pub enum Syscall { GfxDrawCircle = 0x1004, GfxDrawDisc = 0x1005, GfxDrawSquare = 0x1006, - GfxSetSprite = 0x1007, GfxDrawText = 0x1008, GfxClear565 = 0x1010, + ComposerBindScene = 0x1101, + ComposerUnbindScene = 0x1102, + ComposerSetCamera = 0x1103, + ComposerEmitSprite = 0x1104, AudioPlaySample = 0x3001, AudioPlay = 0x3002, FsOpen = 0x4001, diff --git a/crates/console/prometeu-hal/src/syscalls/domains/composer.rs b/crates/console/prometeu-hal/src/syscalls/domains/composer.rs new file mode 100644 index 00000000..32a53a95 --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/composer.rs @@ -0,0 +1,22 @@ +use crate::syscalls::{Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + SyscallRegistryEntry::builder(Syscall::ComposerBindScene, "composer", "bind_scene") + .args(1) + .rets(1) + .caps(caps::GFX) + .cost(5), + SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene") + .rets(1) + .caps(caps::GFX) + .cost(2), + SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera") + .args(2) + .caps(caps::GFX) + .cost(2), + SyscallRegistryEntry::builder(Syscall::ComposerEmitSprite, "composer", "emit_sprite") + .args(9) + .rets(1) + .caps(caps::GFX) + .cost(5), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs b/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs index 95998186..f6023957 100644 --- a/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs +++ b/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs @@ -25,11 +25,6 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ .args(6) .caps(caps::GFX) .cost(5), - SyscallRegistryEntry::builder(Syscall::GfxSetSprite, "gfx", "set_sprite") - .args(10) - .rets(1) - .caps(caps::GFX) - .cost(5), SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text") .args(4) .caps(caps::GFX) diff --git a/crates/console/prometeu-hal/src/syscalls/domains/mod.rs b/crates/console/prometeu-hal/src/syscalls/domains/mod.rs index 3b4d3b34..00e896f1 100644 --- a/crates/console/prometeu-hal/src/syscalls/domains/mod.rs +++ b/crates/console/prometeu-hal/src/syscalls/domains/mod.rs @@ -1,6 +1,7 @@ mod asset; mod audio; mod bank; +mod composer; mod fs; mod gfx; mod log; @@ -12,6 +13,7 @@ pub(crate) fn all_entries() -> impl Iterator Some(Self::GfxDrawCircle), 0x1005 => Some(Self::GfxDrawDisc), 0x1006 => Some(Self::GfxDrawSquare), - 0x1007 => Some(Self::GfxSetSprite), 0x1008 => Some(Self::GfxDrawText), 0x1010 => Some(Self::GfxClear565), + 0x1101 => Some(Self::ComposerBindScene), + 0x1102 => Some(Self::ComposerUnbindScene), + 0x1103 => Some(Self::ComposerSetCamera), + 0x1104 => Some(Self::ComposerEmitSprite), 0x3001 => Some(Self::AudioPlaySample), 0x3002 => Some(Self::AudioPlay), 0x4001 => Some(Self::FsOpen), @@ -68,9 +71,12 @@ impl Syscall { Self::GfxDrawCircle => "GfxDrawCircle", Self::GfxDrawDisc => "GfxDrawDisc", Self::GfxDrawSquare => "GfxDrawSquare", - Self::GfxSetSprite => "GfxSetSprite", Self::GfxDrawText => "GfxDrawText", Self::GfxClear565 => "GfxClear565", + Self::ComposerBindScene => "ComposerBindScene", + Self::ComposerUnbindScene => "ComposerUnbindScene", + Self::ComposerSetCamera => "ComposerSetCamera", + Self::ComposerEmitSprite => "ComposerEmitSprite", Self::AudioPlaySample => "AudioPlaySample", Self::AudioPlay => "AudioPlay", Self::FsOpen => "FsOpen", diff --git a/crates/console/prometeu-hal/src/syscalls/tests.rs b/crates/console/prometeu-hal/src/syscalls/tests.rs index 8f3f55af..9594c17b 100644 --- a/crates/console/prometeu-hal/src/syscalls/tests.rs +++ b/crates/console/prometeu-hal/src/syscalls/tests.rs @@ -194,10 +194,6 @@ fn status_first_syscall_signatures_are_pinned() { assert_eq!(draw_square.arg_slots, 6); assert_eq!(draw_square.ret_slots, 0); - let set_sprite = meta_for(Syscall::GfxSetSprite); - assert_eq!(set_sprite.arg_slots, 10); - assert_eq!(set_sprite.ret_slots, 1); - let draw_text = meta_for(Syscall::GfxDrawText); assert_eq!(draw_text.arg_slots, 4); assert_eq!(draw_text.ret_slots, 0); @@ -206,6 +202,22 @@ fn status_first_syscall_signatures_are_pinned() { assert_eq!(clear_565.arg_slots, 1); assert_eq!(clear_565.ret_slots, 0); + let bind_scene = meta_for(Syscall::ComposerBindScene); + assert_eq!(bind_scene.arg_slots, 1); + assert_eq!(bind_scene.ret_slots, 1); + + let unbind_scene = meta_for(Syscall::ComposerUnbindScene); + assert_eq!(unbind_scene.arg_slots, 0); + assert_eq!(unbind_scene.ret_slots, 1); + + let set_camera = meta_for(Syscall::ComposerSetCamera); + assert_eq!(set_camera.arg_slots, 2); + assert_eq!(set_camera.ret_slots, 0); + + let emit_sprite = meta_for(Syscall::ComposerEmitSprite); + assert_eq!(emit_sprite.arg_slots, 9); + assert_eq!(emit_sprite.ret_slots, 1); + let audio_play_sample = meta_for(Syscall::AudioPlaySample); assert_eq!(audio_play_sample.arg_slots, 5); assert_eq!(audio_play_sample.ret_slots, 1); @@ -231,10 +243,10 @@ fn status_first_syscall_signatures_are_pinned() { fn declared_resolver_rejects_legacy_status_first_signatures() { let declared = vec![ prometeu_bytecode::SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "bind_scene".into(), version: 1, - arg_slots: 10, + arg_slots: 1, ret_slots: 0, }, prometeu_bytecode::SyscallDecl { @@ -306,10 +318,24 @@ fn declared_resolver_rejects_legacy_status_first_signatures() { fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() { let declared = vec![ prometeu_bytecode::SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "bind_scene".into(), version: 1, - arg_slots: 10, + arg_slots: 1, + ret_slots: 1, + }, + prometeu_bytecode::SyscallDecl { + module: "composer".into(), + name: "unbind_scene".into(), + version: 1, + arg_slots: 0, + ret_slots: 1, + }, + prometeu_bytecode::SyscallDecl { + module: "composer".into(), + name: "emit_sprite".into(), + version: 1, + arg_slots: 9, ret_slots: 1, }, prometeu_bytecode::SyscallDecl { @@ -342,8 +368,10 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() { assert_eq!(resolved.len(), declared.len()); assert_eq!(resolved[0].meta.ret_slots, 1); assert_eq!(resolved[1].meta.ret_slots, 1); - assert_eq!(resolved[2].meta.ret_slots, 2); + assert_eq!(resolved[2].meta.ret_slots, 1); assert_eq!(resolved[3].meta.ret_slots, 1); + assert_eq!(resolved[4].meta.ret_slots, 2); + assert_eq!(resolved[5].meta.ret_slots, 1); } #[test] diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs index e3ec62ef..95a00ecb 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -4,14 +4,11 @@ use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef}; use prometeu_hal::cartridge::AppMode; use prometeu_hal::color::Color; -use prometeu_hal::glyph::Glyph; use prometeu_hal::log::{LogLevel, LogSource}; -use prometeu_hal::sprite::Sprite; use prometeu_hal::syscalls::Syscall; use prometeu_hal::vm_fault::VmFault; use prometeu_hal::{ - AudioOpStatus, GfxOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, - expect_int, + AudioOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_int, }; use std::sync::atomic::Ordering; @@ -135,49 +132,6 @@ impl NativeInterface for VirtualMachineRuntime { hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color); Ok(()) } - Syscall::GfxSetSprite => { - let bank_id = expect_int(args, 0)? as u8; - let index = expect_int(args, 1)? as usize; - let x = expect_int(args, 2)? as i32; - let y = expect_int(args, 3)? as i32; - let glyph_id = expect_int(args, 4)? as u16; - let palette_id = expect_int(args, 5)? as u8; - let active = expect_bool(args, 6)?; - let flip_x = expect_bool(args, 7)?; - let flip_y = expect_bool(args, 8)?; - let priority = expect_int(args, 9)? as u8; - - if index >= 512 { - ret.push_int(GfxOpStatus::InvalidSpriteIndex as i64); - return Ok(()); - } - - if hw.assets().slot_info(SlotRef::gfx(bank_id as usize)).asset_id.is_none() { - ret.push_int(GfxOpStatus::BankInvalid as i64); - return Ok(()); - } - - if palette_id >= 64 || priority >= 5 { - ret.push_int(GfxOpStatus::ArgRangeInvalid as i64); - return Ok(()); - } - - if active { - hw.emit_sprite(Sprite { - glyph: Glyph { glyph_id, palette_id }, - x, - y, - layer: 0, - bank_id, - active: false, - flip_x, - flip_y, - priority, - }); - } - ret.push_int(GfxOpStatus::Ok as i64); - Ok(()) - } Syscall::GfxDrawText => { let x = expect_int(args, 0)? as i32; let y = expect_int(args, 1)? as i32; @@ -194,6 +148,13 @@ impl NativeInterface for VirtualMachineRuntime { hw.gfx_mut().clear(Color::from_raw(color_val as u16)); Ok(()) } + Syscall::ComposerBindScene + | Syscall::ComposerUnbindScene + | Syscall::ComposerSetCamera + | Syscall::ComposerEmitSprite => Err(VmFault::Trap( + TRAP_INVALID_SYSCALL, + "Composer syscall support is not implemented yet".into(), + )), Syscall::AudioPlaySample => { let sample_id_raw = expect_int(args, 0)?; let voice_id_raw = expect_int(args, 1)?; diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index 52bbb659..cef12bfa 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -2567,11 +2567,8 @@ mod tests { #[test] fn test_status_first_syscall_results_count_mismatch_panic() { - // GfxSetSprite (0x1007) expects 1 result. - let code = assemble( - "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nSYSCALL 0x1007", - ) - .expect("assemble"); + // ComposerBindScene (0x1101) expects 1 result. + let code = assemble("PUSH_I32 0\nSYSCALL 0x1101").expect("assemble"); struct BadNativeNoReturn; impl NativeInterface for BadNativeNoReturn { @@ -2921,10 +2918,24 @@ mod tests { fn test_loader_patching_accepts_status_first_signatures() { let cases = vec![ SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "bind_scene".into(), version: 1, - arg_slots: 10, + arg_slots: 1, + ret_slots: 1, + }, + SyscallDecl { + module: "composer".into(), + name: "unbind_scene".into(), + version: 1, + arg_slots: 0, + ret_slots: 1, + }, + SyscallDecl { + module: "composer".into(), + name: "emit_sprite".into(), + version: 1, + arg_slots: 9, ret_slots: 1, }, SyscallDecl { @@ -2977,10 +2988,10 @@ mod tests { fn test_loader_patching_rejects_legacy_status_first_ret_slots() { let cases = vec![ SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "bind_scene".into(), version: 1, - arg_slots: 10, + arg_slots: 1, ret_slots: 0, }, SyscallDecl { diff --git a/docs/specs/runtime/04-gfx-peripheral.md b/docs/specs/runtime/04-gfx-peripheral.md index 518744f2..d0e5186f 100644 --- a/docs/specs/runtime/04-gfx-peripheral.md +++ b/docs/specs/runtime/04-gfx-peripheral.md @@ -536,7 +536,12 @@ The system can measure: ## 19. Syscall Return and Fault Policy -`gfx` follows status-first policy for operations with operational failure modes. +Graphics-related public ABI in v1 is split between: + +- `gfx.*` for direct drawing/backend-oriented operations; +- `composer.*` for frame orchestration operations. + +Only operations with real operational rejection paths return explicit status values. Fault boundary: @@ -544,50 +549,50 @@ Fault boundary: - `status`: operational failure; - `Panic`: internal runtime invariant break only. -### 19.1 `gfx.set_sprite` +### 19.1 Return-shape matrix in v1 -Return-shape matrix in v1: +| Syscall | Return | Policy basis | +| ----------------------- | ------------- | --------------------------------------------------- | +| `gfx.clear` | `void` | no real operational failure path in v1 | +| `gfx.fill_rect` | `void` | no real operational failure path in v1 | +| `gfx.draw_line` | `void` | no real operational failure path in v1 | +| `gfx.draw_circle` | `void` | no real operational failure path in v1 | +| `gfx.draw_disc` | `void` | no real operational failure path in v1 | +| `gfx.draw_square` | `void` | no real operational failure path in v1 | +| `gfx.draw_text` | `void` | no real operational failure path in v1 | +| `gfx.clear_565` | `void` | no real operational failure path in v1 | +| `composer.bind_scene` | `status:int` | explicit orchestration-domain operational result | +| `composer.unbind_scene` | `status:int` | explicit orchestration-domain operational result | +| `composer.set_camera` | `void` | no real operational failure path in v1 | +| `composer.emit_sprite` | `status:int` | explicit orchestration-domain operational rejection | -| Syscall | Return | Policy basis | -| ------------------ | ------------- | ---------------------------------------------------- | -| `gfx.clear` | `void` | no real operational failure path in v1 | -| `gfx.fill_rect` | `void` | no real operational failure path in v1 | -| `gfx.draw_line` | `void` | no real operational failure path in v1 | -| `gfx.draw_circle` | `void` | no real operational failure path in v1 | -| `gfx.draw_disc` | `void` | no real operational failure path in v1 | -| `gfx.draw_square` | `void` | no real operational failure path in v1 | -| `gfx.set_sprite` | `status:int` | operational rejection must be explicit | -| `gfx.draw_text` | `void` | no real operational failure path in v1 | -| `gfx.clear_565` | `void` | no real operational failure path in v1 | +### 19.2 `composer.emit_sprite` -Only `gfx.set_sprite` is status-returning in v1. -All other `gfx` syscalls remain `void` unless a future domain revision introduces a real operational failure path. - -### 19.2 `gfx.set_sprite` - -`gfx.set_sprite` returns `status:int`. +`composer.emit_sprite` returns `status:int`. ABI: -1. `bank_id: int` — index of the tile bank -2. `index: int` — sprite index (0..511) +1. `glyph_id: int` — glyph index within the bank +2. `palette_id: int` — palette index 3. `x: int` — x coordinate 4. `y: int` — y coordinate -5. `tile_id: int` — tile index within the bank -6. `palette_id: int` — palette index (0..63) -7. `active: bool` — visibility toggle -8. `flip_x: bool` — horizontal flip -9. `flip_y: bool` — vertical flip -10. `priority: int` — layer priority (0..4) +5. `layer: int` — composition layer reference +6. `bank_id: int` — glyph bank index +7. `flip_x: bool` — horizontal flip +8. `flip_y: bool` — vertical flip +9. `priority: int` — within-layer ordering priority Minimum status table: - `0` = `OK` -- `2` = `INVALID_SPRITE_INDEX` -- `3` = `INVALID_ARG_RANGE` -- `4` = `BANK_INVALID` +- `1` = `SCENE_UNAVAILABLE` +- `2` = `INVALID_ARG_RANGE` +- `3` = `BANK_INVALID` +- `4` = `LAYER_INVALID` +- `5` = `SPRITE_OVERFLOW` Operational notes: -- no fallback to default bank when the sprite bank id cannot be resolved; -- no silent no-op for invalid index/range; -- `palette_id` and `priority` must be validated against runtime-supported ranges. +- the canonical public sprite contract is frame-emission based; +- no caller-provided sprite index exists in the v1 canonical ABI; +- no `active` flag exists in the v1 canonical ABI; +- overflow remains non-fatal and must not escalate to trap in v1. diff --git a/docs/specs/runtime/16-host-abi-and-syscalls.md b/docs/specs/runtime/16-host-abi-and-syscalls.md index 7ca43033..0e2b4c1b 100644 --- a/docs/specs/runtime/16-host-abi-and-syscalls.md +++ b/docs/specs/runtime/16-host-abi-and-syscalls.md @@ -39,6 +39,7 @@ Example: ``` ("gfx", "present", 1) ("audio", "play", 2) +("composer", "emit_sprite", 1) ``` This identity is: @@ -198,6 +199,24 @@ For `asset.load`: - `slot` is the target slot index; - bank kind is resolved from `asset_table` by `asset_id`, not supplied by the caller. +### Composition surface (`composer`, v1) + +The canonical frame-orchestration public ABI uses module `composer`. + +Canonical operations in v1 are: + +- `composer.bind_scene(bank_id) -> (status)` +- `composer.unbind_scene() -> (status)` +- `composer.set_camera(x, y) -> void` +- `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> (status)` + +For mutating composer operations: + +- `status` is a `ComposerOpStatus` value; +- `bind_scene`, `unbind_scene`, and `emit_sprite` are status-returning; +- `set_camera` remains `void` in v1; +- no caller-provided sprite index or `active` flag is part of the canonical contract. + ## 7 Syscalls as Callable Entities (Not First-Class) Syscalls behave like call sites, not like first-class guest values. diff --git a/docs/vm-arch/ISA_CORE.md b/docs/vm-arch/ISA_CORE.md index f2cea12d..d6f704d5 100644 --- a/docs/vm-arch/ISA_CORE.md +++ b/docs/vm-arch/ISA_CORE.md @@ -85,6 +85,9 @@ Example: - `asset.load` currently resolves with `arg_slots = 2` and `ret_slots = 2`. - The canonical stack contract is `asset_id, slot -> status, handle`. - Callers do not provide an explicit asset kind; the runtime derives it from `asset_table`. +- `composer.bind_scene` resolves with `arg_slots = 1` and `ret_slots = 1`. +- The canonical stack contract is `bank_id -> status`. +- `composer.emit_sprite` resolves with `arg_slots = 9` and `ret_slots = 1`. #### Canonical Intrinsic Registry Artifact -- 2.47.2 From cc700c6cf8821101a7e90911c3e48b28ace9fd98 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 17:55:04 +0100 Subject: [PATCH 12/21] implements PLN-0023 --- .../console/prometeu-drivers/src/hardware.rs | 20 ++- .../prometeu-hal/src/hardware_bridge.rs | 6 +- .../src/virtual_machine_runtime/dispatch.rs | 123 +++++++++++++- .../src/virtual_machine_runtime/tests.rs | 158 ++++++++++++++---- 4 files changed, 261 insertions(+), 46 deletions(-) diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index c9de3447..d648356b 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -51,14 +51,30 @@ impl HardwareBridge for Hardware { self.frame_composer.begin_frame(); } - fn emit_sprite(&mut self, sprite: Sprite) { - let _ = self.frame_composer.emit_sprite(sprite); + fn bind_scene(&mut self, scene_bank_id: usize) -> bool { + self.frame_composer.bind_scene(scene_bank_id) + } + + fn unbind_scene(&mut self) { + self.frame_composer.unbind_scene(); + } + + fn set_camera(&mut self, x: i32, y: i32) { + self.frame_composer.set_camera(x, y); + } + + fn emit_sprite(&mut self, sprite: Sprite) -> bool { + self.frame_composer.emit_sprite(sprite) } fn render_frame(&mut self) { self.frame_composer.render_frame(&mut self.gfx); } + fn has_glyph_bank(&self, bank_id: usize) -> bool { + self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some() + } + fn gfx(&self) -> &dyn GfxBridge { &self.gfx } diff --git a/crates/console/prometeu-hal/src/hardware_bridge.rs b/crates/console/prometeu-hal/src/hardware_bridge.rs index 2bcb3376..28ccb77f 100644 --- a/crates/console/prometeu-hal/src/hardware_bridge.rs +++ b/crates/console/prometeu-hal/src/hardware_bridge.rs @@ -7,8 +7,12 @@ use crate::touch_bridge::TouchBridge; pub trait HardwareBridge { fn begin_frame(&mut self); - fn emit_sprite(&mut self, sprite: Sprite); + fn bind_scene(&mut self, scene_bank_id: usize) -> bool; + fn unbind_scene(&mut self); + fn set_camera(&mut self, x: i32, y: i32); + fn emit_sprite(&mut self, sprite: Sprite) -> bool; fn render_frame(&mut self); + fn has_glyph_bank(&self, bank_id: usize) -> bool; fn gfx(&self) -> &dyn GfxBridge; fn gfx_mut(&mut self) -> &mut dyn GfxBridge; diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs index 95a00ecb..2a0fb39b 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -4,11 +4,14 @@ use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef}; use prometeu_hal::cartridge::AppMode; use prometeu_hal::color::Color; +use prometeu_hal::glyph::Glyph; use prometeu_hal::log::{LogLevel, LogSource}; +use prometeu_hal::sprite::Sprite; use prometeu_hal::syscalls::Syscall; use prometeu_hal::vm_fault::VmFault; use prometeu_hal::{ - AudioOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_int, + AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, + expect_bool, expect_int, }; use std::sync::atomic::Ordering; @@ -53,6 +56,23 @@ impl VirtualMachineRuntime { pub(crate) fn get_color(&self, value: i64) -> Color { Color::from_raw(value as u16) } + + fn int_arg_to_usize_status(value: i64) -> Result { + usize::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid) + } + + fn int_arg_to_i32_trap(value: i64, name: &str) -> Result { + i32::try_from(value) + .map_err(|_| VmFault::Trap(TRAP_OOB, format!("{name} value out of bounds"))) + } + + fn int_arg_to_u8_status(value: i64) -> Result { + u8::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid) + } + + fn int_arg_to_u16_status(value: i64) -> Result { + u16::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid) + } } impl NativeInterface for VirtualMachineRuntime { @@ -148,13 +168,100 @@ impl NativeInterface for VirtualMachineRuntime { hw.gfx_mut().clear(Color::from_raw(color_val as u16)); Ok(()) } - Syscall::ComposerBindScene - | Syscall::ComposerUnbindScene - | Syscall::ComposerSetCamera - | Syscall::ComposerEmitSprite => Err(VmFault::Trap( - TRAP_INVALID_SYSCALL, - "Composer syscall support is not implemented yet".into(), - )), + Syscall::ComposerBindScene => { + let scene_bank_id = match Self::int_arg_to_usize_status(expect_int(args, 0)?) { + Ok(id) => id, + Err(status) => { + ret.push_int(status as i64); + return Ok(()); + } + }; + + let status = if hw.bind_scene(scene_bank_id) { + ComposerOpStatus::Ok + } else { + ComposerOpStatus::SceneUnavailable + }; + ret.push_int(status as i64); + Ok(()) + } + Syscall::ComposerUnbindScene => { + hw.unbind_scene(); + ret.push_int(ComposerOpStatus::Ok as i64); + Ok(()) + } + Syscall::ComposerSetCamera => { + let x = Self::int_arg_to_i32_trap(expect_int(args, 0)?, "camera x")?; + let y = Self::int_arg_to_i32_trap(expect_int(args, 1)?, "camera y")?; + hw.set_camera(x, y); + Ok(()) + } + Syscall::ComposerEmitSprite => { + let glyph_id = match Self::int_arg_to_u16_status(expect_int(args, 0)?) { + Ok(value) => value, + Err(status) => { + ret.push_int(status as i64); + return Ok(()); + } + }; + let palette_id = match Self::int_arg_to_u8_status(expect_int(args, 1)?) { + Ok(value) if value < 64 => value, + _ => { + ret.push_int(ComposerOpStatus::ArgRangeInvalid as i64); + return Ok(()); + } + }; + let x = Self::int_arg_to_i32_trap(expect_int(args, 2)?, "sprite x")?; + let y = Self::int_arg_to_i32_trap(expect_int(args, 3)?, "sprite y")?; + let layer = match Self::int_arg_to_u8_status(expect_int(args, 4)?) { + Ok(value) if value < 4 => value, + Ok(_) => { + ret.push_int(ComposerOpStatus::LayerInvalid as i64); + return Ok(()); + } + Err(status) => { + ret.push_int(status as i64); + return Ok(()); + } + }; + let bank_id = match Self::int_arg_to_u8_status(expect_int(args, 5)?) { + Ok(value) => value, + Err(status) => { + ret.push_int(status as i64); + return Ok(()); + } + }; + let flip_x = expect_bool(args, 6)?; + let flip_y = expect_bool(args, 7)?; + let priority = match Self::int_arg_to_u8_status(expect_int(args, 8)?) { + Ok(value) => value, + Err(status) => { + ret.push_int(status as i64); + return Ok(()); + } + }; + + if !hw.has_glyph_bank(bank_id as usize) { + ret.push_int(ComposerOpStatus::BankInvalid as i64); + return Ok(()); + } + + let emitted = hw.emit_sprite(Sprite { + glyph: Glyph { glyph_id, palette_id }, + x, + y, + layer, + bank_id, + active: false, + flip_x, + flip_y, + priority, + }); + let status = + if emitted { ComposerOpStatus::Ok } else { ComposerOpStatus::SpriteOverflow }; + ret.push_int(status as i64); + Ok(()) + } Syscall::AudioPlaySample => { let sample_id_raw = expect_int(args, 0)?; let voice_id_raw = expect_int(args, 1)?; diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index 53df08e8..68780ef9 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -7,7 +7,7 @@ use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, use prometeu_drivers::hardware::Hardware; use prometeu_drivers::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller}; use prometeu_hal::AudioOpStatus; -use prometeu_hal::GfxOpStatus; +use prometeu_hal::ComposerOpStatus; use prometeu_hal::InputSignals; use prometeu_hal::asset::{ AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus, @@ -298,6 +298,62 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() { assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); } +#[test] +fn tick_renders_scene_through_public_composer_syscalls() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let signals = InputSignals::default(); + let code = assemble( + "PUSH_I32 0\nHOSTCALL 0\nPOP_N 1\n\ + PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 1\n\ + PUSH_I32 0\nPUSH_I32 2\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 2\n\ + FRAME_SYNC\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module( + code, + vec![ + SyscallDecl { + module: "composer".into(), + name: "bind_scene".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }, + SyscallDecl { + module: "composer".into(), + name: "set_camera".into(), + version: 1, + arg_slots: 2, + ret_slots: 0, + }, + SyscallDecl { + module: "composer".into(), + name: "emit_sprite".into(), + version: 1, + arg_slots: 9, + ret_slots: 1, + }, + ], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE))); + banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8))); + let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks)); + hardware.gfx.scene_fade_level = 31; + hardware.gfx.hud_fade_level = 31; + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick(&mut vm, &signals, &mut hardware); + + assert!(report.is_none(), "public composer path must not crash"); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]); + hardware.gfx.present(); + assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); +} + #[test] fn initialize_vm_success_clears_previous_crash_report() { let mut runtime = VirtualMachineRuntime::new(None); @@ -429,22 +485,19 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() { } #[test] -fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() { +fn tick_composer_bind_scene_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); - let code = assemble( - "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", - ) - .expect("assemble"); + let code = assemble("PUSH_I32 99\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "bind_scene".into(), version: 1, - arg_slots: 10, + arg_slots: 1, ret_slots: 1, }], ); @@ -454,26 +507,29 @@ fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() { let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); - assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::BankInvalid as i64)]); + assert_eq!( + vm.operand_stack_top(1), + vec![Value::Int64(ComposerOpStatus::SceneUnavailable as i64)] + ); } #[test] -fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() { +fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( - "PUSH_I32 0\nPUSH_I32 512\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", + "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "emit_sprite".into(), version: 1, - arg_slots: 10, + arg_slots: 9, ret_slots: 1, }], ); @@ -481,28 +537,57 @@ fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() { runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); - assert!(report.is_none(), "invalid sprite index must not crash"); + assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); - assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)]); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]); } #[test] -fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() { +fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( - "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 64\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", + "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 4\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "emit_sprite".into(), version: 1, - arg_slots: 10, + arg_slots: 9, + ret_slots: 1, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick(&mut vm, &signals, &mut hardware); + assert!(report.is_none(), "invalid layer must not crash"); + assert!(vm.is_halted()); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::LayerInvalid as i64)]); +} + +#[test] +fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble( + "PUSH_I32 0\nPUSH_I32 64\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "composer".into(), + name: "emit_sprite".into(), + version: 1, + arg_slots: 9, ret_slots: 1, }], ); @@ -517,9 +602,12 @@ fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() { runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); - assert!(report.is_none(), "invalid gfx parameter range must not crash"); + assert!(report.is_none(), "invalid composer parameter range must not crash"); assert!(vm.is_halted()); - assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::ArgRangeInvalid as i64)]); + assert_eq!( + vm.operand_stack_top(1), + vec![Value::Int64(ComposerOpStatus::ArgRangeInvalid as i64)] + ); } #[test] @@ -881,13 +969,13 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { } #[test] -fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() { +fn tick_status_first_surface_smoke_across_composer_audio_and_asset() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( - "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\ + "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\ PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 1\n\ PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 2\n\ HALT" @@ -897,10 +985,10 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() { code, vec![ SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "emit_sprite".into(), version: 1, - arg_slots: 10, + arg_slots: 9, ret_slots: 1, }, SyscallDecl { @@ -931,28 +1019,28 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() { Value::Int64(0), Value::Int64(AssetLoadError::AssetNotFound as i64), Value::Int64(AudioOpStatus::BankInvalid as i64), - Value::Int64(GfxOpStatus::BankInvalid as i64), + Value::Int64(ComposerOpStatus::BankInvalid as i64), ] ); } #[test] -fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() { +fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( - "PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", + "PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "emit_sprite".into(), version: 1, - arg_slots: 10, + arg_slots: 9, ret_slots: 1, }], ); -- 2.47.2 From 3fef407efcd1a415c5664308cde03f1ba0b95a53 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 17:56:37 +0100 Subject: [PATCH 13/21] implements PLN-0024 --- .../console/prometeu-hal/src/syscalls/tests.rs | 12 ++++++++++++ crates/tools/pbxgen-stress/src/lib.rs | 10 +++++----- test-cartridges/stress-console/program.pbx | Bin 942 -> 945 bytes 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/console/prometeu-hal/src/syscalls/tests.rs b/crates/console/prometeu-hal/src/syscalls/tests.rs index 9594c17b..3129d813 100644 --- a/crates/console/prometeu-hal/src/syscalls/tests.rs +++ b/crates/console/prometeu-hal/src/syscalls/tests.rs @@ -63,6 +63,18 @@ fn resolver_rejects_unknown_identity() { } } +#[test] +fn resolver_rejects_removed_legacy_gfx_set_sprite_identity() { + assert!(resolve_syscall("gfx", "set_sprite", 1).is_none()); + + let requested = [SyscallIdentity { module: "gfx", name: "set_sprite", version: 1 }]; + let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err(); + assert_eq!( + err, + LoadError::UnknownSyscall { module: "gfx".into(), name: "set_sprite".into(), version: 1 } + ); +} + #[test] fn resolver_enforces_capabilities() { let requested = [SyscallIdentity { module: "gfx", name: "clear", version: 1 }]; diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index 79a42dfc..d8137e23 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -42,10 +42,10 @@ pub fn generate() -> Result<()> { ret_slots: 0, }, SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "emit_sprite".into(), version: 1, - arg_slots: 10, + arg_slots: 9, ret_slots: 1, }, ]; @@ -124,9 +124,9 @@ fn heavy_load(rom: &mut Vec) { // --- clear screen --- rom.extend(asm("PUSH_I32 0\nHOSTCALL 0")); - // --- call status-first syscall path once per frame and drop status --- + // --- call composer-domain sprite emission path once per frame and drop status --- rom.extend(asm( - "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 4\nPOP_N 1", + "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 4\nPOP_N 1", )); // --- draw 500 discs --- diff --git a/test-cartridges/stress-console/program.pbx b/test-cartridges/stress-console/program.pbx index 83b01e1922b659ed64b42e6d32f4ebc2b04ceb63..033fc6c94a313e9b9a947d2f48a59248a1829cba 100644 GIT binary patch delta 164 zcmZ3-zL9-`gK7>F0|PSy1A_#R5CPIGK&%5~3jk?WAhrRrODCpUGv-WOm^)dJQI9cc zvIFB;#)ioXjN+Ui9Y#R?R+IIZ;(6aOLi8~(Ffn|YJd4R&HV-J`093=kAkJU|BvpVk s2p9nMWK3pao+``1ker`ekYAiy#LbYJn^_WHTu_u*lFG=y$-u|}04Tp3^8f$< delta 158 zcmdnUzK(r@gK9n#0|PSy1A{b>5CPIGK&%gB3jk?WAhrjx^CzZSGv-fRm^+!9(U389 zvMu9T#^%YgjN+Ui9Y#R?wv)A);(0$ZLi8~(Ffn|eJe|o~wh$=d093=kAkJU|BvpVk m2p9nMWKaIjG*z6LAw8{vi=jBRB)+(yD6=G$k%5bWkpTdc+Zt^E -- 2.47.2 From 4a5210f3471a71d84933467e3414860008696b8d Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 19:45:03 +0100 Subject: [PATCH 14/21] implements PLN-0025 -- 2.47.2 From b0b8bb90283b598b5be40753912335f356df5933 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 18 Apr 2026 09:50:13 +0100 Subject: [PATCH 15/21] primitives pipeline adjustments --- Cargo.lock | 2 + crates/tools/pbxgen-stress/Cargo.toml | 2 + crates/tools/pbxgen-stress/src/lib.rs | 419 +++++++++++++----- discussion/index.ndjson | 3 +- ...erred-overlay-and-primitive-composition.md | 140 ++++++ ...rred-gfx-overlay-outside-frame-composer.md | 150 +++++++ ...x-overlay-contract-and-spec-propagation.md | 93 ++++ ...PLN-0027-deferred-gfx-overlay-subsystem.md | 104 +++++ ...rame-end-overlay-integration-and-parity.md | 106 +++++ ...-final-overlay-ci-validation-and-polish.md | 82 ++++ test-cartridges/stress-console/assets.pa | Bin 0 -> 35432 bytes test-cartridges/stress-console/manifest.json | 2 +- test-cartridges/stress-console/program.pbx | Bin 945 -> 984 bytes 13 files changed, 1002 insertions(+), 101 deletions(-) create mode 100644 discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md create mode 100644 discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md create mode 100644 discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md create mode 100644 discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md create mode 100644 discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md create mode 100644 discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md create mode 100644 test-cartridges/stress-console/assets.pa diff --git a/Cargo.lock b/Cargo.lock index ada203b3..e3fbe577 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,6 +1475,8 @@ version = "0.1.0" dependencies = [ "anyhow", "prometeu-bytecode", + "prometeu-hal", + "serde_json", ] [[package]] diff --git a/crates/tools/pbxgen-stress/Cargo.toml b/crates/tools/pbxgen-stress/Cargo.toml index b2ce9839..31da4f74 100644 --- a/crates/tools/pbxgen-stress/Cargo.toml +++ b/crates/tools/pbxgen-stress/Cargo.toml @@ -5,4 +5,6 @@ edition = "2021" [dependencies] prometeu-bytecode = { path = "../../console/prometeu-bytecode" } +prometeu-hal = { path = "../../console/prometeu-hal" } anyhow = "1" +serde_json = "1" diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index d8137e23..2600c12d 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -3,7 +3,25 @@ use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{ BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl, }; +use prometeu_hal::asset::{ + AssetCodec, AssetEntry, BankType, PreloadEntry, SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1, + SCENE_LAYER_COUNT_V1, SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1, +}; +use prometeu_hal::cartridge::{ + AssetsPackHeader, AssetsPackPrelude, ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE, + ASSETS_PA_SCHEMA_VERSION, +}; +use prometeu_hal::color::Color; +use prometeu_hal::glyph::Glyph; +use prometeu_hal::glyph_bank::{ + TileSize, GLYPH_BANK_COLORS_PER_PALETTE, GLYPH_BANK_PALETTE_COUNT_V1, +}; +use prometeu_hal::scene_bank::SceneBank; +use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; +use prometeu_hal::tile::Tile; +use prometeu_hal::tilemap::TileMap; use std::fs; +use std::mem::size_of; use std::path::PathBuf; fn asm(s: &str) -> Vec { @@ -20,13 +38,6 @@ pub fn generate() -> Result<()> { arg_slots: 1, ret_slots: 0, }, - SyscallDecl { - module: "gfx".into(), - name: "draw_disc".into(), - version: 1, - arg_slots: 5, - ret_slots: 0, - }, SyscallDecl { module: "gfx".into(), name: "draw_text".into(), @@ -41,6 +52,20 @@ pub fn generate() -> Result<()> { arg_slots: 2, ret_slots: 0, }, + SyscallDecl { + module: "composer".into(), + name: "bind_scene".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }, + SyscallDecl { + module: "composer".into(), + name: "set_camera".into(), + version: 1, + arg_slots: 2, + ret_slots: 0, + }, SyscallDecl { module: "composer".into(), name: "emit_sprite".into(), @@ -59,7 +84,7 @@ pub fn generate() -> Result<()> { param_slots: 0, local_slots: 2, return_slots: 0, - max_stack_slots: 16, + max_stack_slots: 32, }]; let module = BytecodeModule { @@ -67,7 +92,6 @@ pub fn generate() -> Result<()> { const_pool: vec![ ConstantPoolEntry::String("stress".into()), ConstantPoolEntry::String("frame".into()), - ConstantPoolEntry::String("missing_glyph_bank".into()), ], functions, code: rom, @@ -89,129 +113,326 @@ pub fn generate() -> Result<()> { out_dir.push("stress-console"); fs::create_dir_all(&out_dir)?; fs::write(out_dir.join("program.pbx"), bytes)?; - let assets_pa_path = out_dir.join("assets.pa"); - if assets_pa_path.exists() { - fs::remove_file(&assets_pa_path)?; - } - fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\"]\n}\n")?; + fs::write(out_dir.join("assets.pa"), build_assets_pack()?)?; + fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\", \"asset\"]\n}\n")?; Ok(()) } -#[allow(dead_code)] fn heavy_load(rom: &mut Vec) { // Single function 0: main - // Everything runs here — no coroutines, no SPAWN, no YIELD. - // - // Global 0 = t (frame counter) - // Local 0 = scratch - // Local 1 = loop counter for discs - // - // Loop: - // t = (t + 1) - // clear screen - // draw 500 discs using t for animation - // draw 20 texts using t for animation - // RET (runtime handles the frame loop) + // Global 0 = frame counter + // Global 1 = scene bound flag + // Local 0 = sprite row + // Local 1 = sprite col - // --- init locals --- - // local 0: scratch - // local 1: loop counter for discs - rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0\nPUSH_I32 0\nSET_LOCAL 1")); - - // --- t = (t + 1) --- - // t is global 0 to persist across prepare_call resets rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0")); - // --- clear screen --- + rom.extend(asm("GET_GLOBAL 1\nPUSH_I32 0\nEQ")); + let jif_bind_done_offset = rom.len() + 2; + rom.extend(asm("JMP_IF_FALSE 0")); + + rom.extend(asm("PUSH_I32 0\nHOSTCALL 3\nPOP_N 1\nPUSH_I32 1\nSET_GLOBAL 1")); + let bind_done_target = rom.len() as u32; + rom.extend(asm("PUSH_I32 0\nHOSTCALL 0")); - // --- call composer-domain sprite emission path once per frame and drop status --- + rom.extend(asm( - "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 4\nPOP_N 1", + "GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 192\nMOD\nGET_GLOBAL 0\nPUSH_I32 76\nMOD\nHOSTCALL 4", )); - // --- draw 500 discs --- - rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); - let disc_loop_start = rom.len() as u32; - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 500\nLT")); - let jif_disc_end_offset = rom.len() + 2; + rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0")); + let row_loop_start = rom.len() as u32; + rom.extend(asm("GET_LOCAL 0\nPUSH_I32 16\nLT")); + let jif_row_end_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); - // x = (t * (i+7) + i * 13) % 320 - rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 7\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 13\nMUL\nADD\nPUSH_I32 320\nMOD")); - // y = (t * (i+11) + i * 17) % 180 - rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 11\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 17\nMUL\nADD\nPUSH_I32 180\nMOD")); - // r = ( (i*13) % 20 ) + 5 - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 13\nMUL\nPUSH_I32 20\nMOD\nPUSH_I32 5\nADD")); - // border color = (i * 1234) & 0xFFFF - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1234\nMUL\nPUSH_I32 65535\nBIT_AND")); - // fill color = (i * 5678 + t) & 0xFFFF - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 5678\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 65535\nBIT_AND")); - // HOSTCALL gfx.draw_disc (x, y, r, border, fill) - rom.extend(asm("HOSTCALL 1")); - - // i++ - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); - let jmp_disc_loop_offset = rom.len() + 2; - rom.extend(asm("JMP 0")); - let disc_loop_end = rom.len() as u32; - - // --- draw 20 texts --- rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); - let text_loop_start = rom.len() as u32; - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 20\nLT")); - let jif_text_end_offset = rom.len() + 2; + let col_loop_start = rom.len() as u32; + rom.extend(asm("GET_LOCAL 1\nPUSH_I32 32\nLT")); + let jif_col_end_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); - // x = (t * 3 + i * 40) % 320 rom.extend(asm( - "GET_GLOBAL 0\nPUSH_I32 3\nMUL\nGET_LOCAL 1\nPUSH_I32 40\nMUL\nADD\nPUSH_I32 320\nMOD", + "PUSH_I32 0\n\ + GET_LOCAL 0\nPUSH_I32 32\nMUL\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 15\nMOD\nPUSH_I32 1\nADD\n\ + GET_LOCAL 1\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 320\nMOD\n\ + GET_LOCAL 0\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 180\nMOD\n\ + GET_LOCAL 0\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 4\nMOD\n\ + PUSH_I32 0\n\ + GET_LOCAL 1\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\ + GET_LOCAL 0\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\ + GET_LOCAL 0\nGET_LOCAL 1\nADD\nPUSH_I32 4\nMOD\n\ + HOSTCALL 5\nPOP_N 1", )); - // y = (i * 30 + t) % 180 - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 30\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 180\nMOD")); - // string (toggle between "stress" and "frame") - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ")); - let jif_text_alt_offset = rom.len() + 2; - rom.extend(asm("JMP_IF_FALSE 0")); - rom.extend(asm("PUSH_CONST 0")); // "stress" - let jmp_text_join_offset = rom.len() + 2; - rom.extend(asm("JMP 0")); - let text_alt_target = rom.len() as u32; - rom.extend(asm("PUSH_CONST 1")); // "frame" - let text_join_target = rom.len() as u32; - // color = (t * 10 + i * 1000) & 0xFFFF - rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 10\nMUL\nGET_LOCAL 1\nPUSH_I32 1000\nMUL\nADD\nPUSH_I32 65535\nBIT_AND")); - // HOSTCALL gfx.draw_text (x, y, str, color) - rom.extend(asm("HOSTCALL 2")); - - // i++ rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); - let jmp_text_loop_offset = rom.len() + 2; + let jmp_col_loop_offset = rom.len() + 2; rom.extend(asm("JMP 0")); - let text_loop_end = rom.len() as u32; + let col_loop_end = rom.len() as u32; + + rom.extend(asm("GET_LOCAL 0\nPUSH_I32 1\nADD\nSET_LOCAL 0")); + let jmp_row_loop_offset = rom.len() + 2; + rom.extend(asm("JMP 0")); + let row_loop_end = rom.len() as u32; + + rom.extend(asm( + "PUSH_I32 8\n\ + PUSH_I32 8\n\ + PUSH_CONST 0\n\ + GET_GLOBAL 0\nPUSH_I32 2047\nMUL\nPUSH_I32 65535\nBIT_AND\n\ + HOSTCALL 1", + )); + rom.extend(asm( + "PUSH_I32 8\n\ + PUSH_I32 20\n\ + PUSH_CONST 1\n\ + GET_GLOBAL 0\nPUSH_I32 4093\nMUL\nPUSH_I32 65535\nBIT_AND\n\ + HOSTCALL 1", + )); - // --- log every 60 frames --- rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ")); let jif_log_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); - rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 3")); + rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 2")); let after_log = rom.len() as u32; - // --- end of function --- rom.extend(asm("FRAME_SYNC\nRET")); - // --- Patch jump targets --- let patch = |buf: &mut Vec, imm_offset: usize, target: u32| { buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes()); }; - patch(rom, jif_disc_end_offset, disc_loop_end); - patch(rom, jmp_disc_loop_offset, disc_loop_start); - - patch(rom, jif_text_end_offset, text_loop_end); - patch(rom, jif_text_alt_offset, text_alt_target); - patch(rom, jmp_text_join_offset, text_join_target); - patch(rom, jmp_text_loop_offset, text_loop_start); - + patch(rom, jif_bind_done_offset, bind_done_target); + patch(rom, jif_row_end_offset, row_loop_end); + patch(rom, jif_col_end_offset, col_loop_end); + patch(rom, jmp_col_loop_offset, col_loop_start); + patch(rom, jmp_row_loop_offset, row_loop_start); patch(rom, jif_log_offset, after_log); } + +fn build_assets_pack() -> Result> { + let (glyph_entry, glyph_payload) = build_glyph_asset(); + let scene = build_scene_bank(); + let scene_payload = encode_scene_payload(&scene); + let scene_entry = AssetEntry { + asset_id: 1, + asset_name: "stress_scene".into(), + bank_type: BankType::SCENE, + offset: glyph_payload.len() as u64, + size: scene_payload.len() as u64, + decoded_size: expected_scene_decoded_size(&scene) as u64, + codec: AssetCodec::None, + metadata: serde_json::json!({}), + }; + + let asset_table = vec![glyph_entry, scene_entry]; + let preload = + vec![PreloadEntry { asset_id: 0, slot: 0 }, PreloadEntry { asset_id: 1, slot: 0 }]; + let payload_len = glyph_payload.len() + scene_payload.len(); + let header = serde_json::to_vec(&AssetsPackHeader { asset_table, preload })?; + let payload_offset = (ASSETS_PA_PRELUDE_SIZE + header.len()) as u64; + let prelude = AssetsPackPrelude { + magic: ASSETS_PA_MAGIC, + schema_version: ASSETS_PA_SCHEMA_VERSION, + header_len: header.len() as u32, + payload_offset, + flags: 0, + reserved: 0, + header_checksum: 0, + }; + + let mut bytes = prelude.to_bytes().to_vec(); + bytes.extend_from_slice(&header); + bytes.extend_from_slice(&glyph_payload); + bytes.extend_from_slice(&scene_payload); + debug_assert_eq!(bytes.len(), payload_offset as usize + payload_len); + Ok(bytes) +} + +fn build_glyph_asset() -> (AssetEntry, Vec) { + let pixel_indices = vec![1_u8; 8 * 8]; + let mut payload = pack_4bpp(&pixel_indices); + payload.extend_from_slice(&build_palette_bytes()); + + let entry = AssetEntry { + asset_id: 0, + asset_name: "stress_square".into(), + bank_type: BankType::GLYPH, + offset: 0, + size: payload.len() as u64, + decoded_size: (8 * 8 + GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2) + as u64, + codec: AssetCodec::None, + metadata: serde_json::json!({ + "tile_size": 8, + "width": 8, + "height": 8, + "palette_count": GLYPH_BANK_PALETTE_COUNT_V1, + "palette_authored": GLYPH_BANK_PALETTE_COUNT_V1 + }), + }; + + (entry, payload) +} + +fn build_palette_bytes() -> Vec { + let mut bytes = + Vec::with_capacity(GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2); + for palette_id in 0..GLYPH_BANK_PALETTE_COUNT_V1 { + for color_index in 0..GLYPH_BANK_COLORS_PER_PALETTE { + let color = if color_index == 1 { stress_color(palette_id) } else { Color::BLACK }; + bytes.extend_from_slice(&color.raw().to_le_bytes()); + } + } + bytes +} + +fn stress_color(palette_id: usize) -> Color { + let r = ((palette_id * 53) % 256) as u8; + let g = ((palette_id * 97 + 64) % 256) as u8; + let b = ((palette_id * 29 + 128) % 256) as u8; + Color::rgb(r, g, b) +} + +fn pack_4bpp(indices: &[u8]) -> Vec { + let mut packed = Vec::with_capacity(indices.len().div_ceil(2)); + for chunk in indices.chunks(2) { + let hi = chunk[0] & 0x0f; + let lo = chunk.get(1).copied().unwrap_or(0) & 0x0f; + packed.push((hi << 4) | lo); + } + packed +} + +fn build_scene_bank() -> SceneBank { + let mut layers = std::array::from_fn(|layer_index| { + let mut tiles = vec![ + Tile { + active: false, + glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 }, + flip_x: false, + flip_y: false, + }; + 64 * 32 + ]; + + for step in 0..8 { + let x = 4 + step * 7 + layer_index * 2; + let y = 2 + step * 3 + layer_index * 2; + let index = y * 64 + x; + tiles[index] = Tile { + active: true, + glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 }, + flip_x: false, + flip_y: false, + }; + } + + SceneLayer { + active: true, + glyph_bank_id: 0, + tile_size: TileSize::Size8, + parallax_factor: match layer_index { + 0 => ParallaxFactor { x: 1.0, y: 1.0 }, + 1 => ParallaxFactor { x: 0.75, y: 0.75 }, + 2 => ParallaxFactor { x: 0.5, y: 0.5 }, + _ => ParallaxFactor { x: 0.25, y: 0.25 }, + }, + tilemap: TileMap { width: 64, height: 32, tiles }, + } + }); + + // Keep the farthest layer a bit sparser so the diagonal remains visually readable. + for step in 0..4 { + let x = 10 + step * 12; + let y = 4 + step * 5; + let index = y * 64 + x; + layers[3].tilemap.tiles[index].active = false; + } + + SceneBank { layers } +} + +fn expected_scene_decoded_size(scene: &SceneBank) -> usize { + scene + .layers + .iter() + .map(|layer| { + SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1 + layer.tilemap.tiles.len() * size_of::() + }) + .sum() +} + +fn encode_scene_payload(scene: &SceneBank) -> Vec { + let mut data = Vec::new(); + data.extend_from_slice(&SCENE_PAYLOAD_MAGIC_V1); + data.extend_from_slice(&SCENE_PAYLOAD_VERSION_V1.to_le_bytes()); + data.extend_from_slice(&(SCENE_LAYER_COUNT_V1 as u16).to_le_bytes()); + data.extend_from_slice(&0_u32.to_le_bytes()); + + for layer in &scene.layers { + let layer_flags = if layer.active { 0b0000_0001 } else { 0 }; + data.push(layer_flags); + data.push(layer.glyph_bank_id); + data.push(layer.tile_size as u8); + data.push(0); + data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes()); + data.extend_from_slice(&layer.parallax_factor.y.to_le_bytes()); + data.extend_from_slice(&(layer.tilemap.width as u32).to_le_bytes()); + data.extend_from_slice(&(layer.tilemap.height as u32).to_le_bytes()); + data.extend_from_slice(&(layer.tilemap.tiles.len() as u32).to_le_bytes()); + data.extend_from_slice(&0_u32.to_le_bytes()); + + for tile in &layer.tilemap.tiles { + let mut tile_flags = 0_u8; + if tile.active { + tile_flags |= 0b0000_0001; + } + if tile.flip_x { + tile_flags |= 0b0000_0010; + } + if tile.flip_y { + tile_flags |= 0b0000_0100; + } + data.push(tile_flags); + data.push(tile.glyph.palette_id); + data.extend_from_slice(&tile.glyph.glyph_id.to_le_bytes()); + } + } + + data +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn assets_pack_contains_preloaded_glyph_and_scene_assets() { + let bytes = build_assets_pack().expect("assets pack"); + let prelude = + AssetsPackPrelude::from_bytes(&bytes[..ASSETS_PA_PRELUDE_SIZE]).expect("prelude"); + assert_eq!(prelude.magic, ASSETS_PA_MAGIC); + assert_eq!(prelude.schema_version, ASSETS_PA_SCHEMA_VERSION); + + let header_start = ASSETS_PA_PRELUDE_SIZE; + let header_end = header_start + prelude.header_len as usize; + let header: AssetsPackHeader = + serde_json::from_slice(&bytes[header_start..header_end]).expect("header"); + + assert_eq!(header.asset_table.len(), 2); + assert_eq!(header.preload.len(), 2); + assert_eq!(header.asset_table[0].bank_type, BankType::GLYPH); + assert_eq!(header.asset_table[1].bank_type, BankType::SCENE); + assert_eq!(header.preload[0].slot, 0); + assert_eq!(header.preload[1].slot, 0); + assert_eq!(header.asset_table[0].offset, 0); + assert_eq!(header.asset_table[1].offset, header.asset_table[0].size); + assert_eq!( + bytes.len(), + prelude.payload_offset as usize + + header.asset_table[0].size as usize + + header.asset_table[1].size as usize + ); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index c8617971..68838273 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":28,"AGD":28,"DEC":16,"PLN":26,"LSN":31,"CLSN":1}} +{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"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"}]} @@ -20,6 +20,7 @@ {"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-15","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-15"}],"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-15","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-15","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-15","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-15","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-15","ref_decisions":["DEC-0014"]}],"lessons":[]} {"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0028","status":"open","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[{"id":"AGD-0028","file":"workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18"}],"decisions":[{"id":"DEC-0016","file":"workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_agenda":"AGD-0028"}],"plans":[{"id":"PLN-0026","file":"workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0027","file":"workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0028","file":"workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0029","file":"workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]}],"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"}]} diff --git a/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md b/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md new file mode 100644 index 00000000..677f31b7 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md @@ -0,0 +1,140 @@ +--- +id: AGD-0028 +ticket: deferred-overlay-and-primitive-composition +title: Deferred Overlay and Primitive Composition over FrameComposer +status: accepted +created: 2026-04-18 +updated: 2026-04-18 +resolved: 2026-04-18 +decision: DEC-0016 +tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud] +--- + +## Contexto + +`FrameComposer.render_frame()` hoje recompõe o `back` no fim da logical frame. Quando há scene bound, o caminho `render_scene_from_cache(...)` limpa o buffer e desenha scene + sprites, o que apaga qualquer primitive ou `draw_text(...)` emitido antes via `gfx`. + +Isso expôs um conflito de modelo: + +- `composer.*` já é o caminho canônico de orquestração de frame; +- `gfx.draw_text(...)` e demais primitives ainda escrevem diretamente no `back`; +- o runtime só chama `render_frame()` no final do frame, então a escrita imediata em `back` deixou de ser semanticamente estável. +- As primitives de `gfx` não são o mecanismo desejado para composição de jogos com scene/tile/sprite; elas existem principalmente como debug, instrumentação visual e artefatos rápidos. + +Conteúdo relevante migrado de [AGD-0010](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md): + +- a arquitetura aceita continua sendo de framebuffer destrutivo em memória, não scene graph ou renderer tipo GPU; +- otimizações em primitives devem preservar a semântica observável, mesmo quando ganharem fast paths internos; +- existe preocupação explícita com custo por classe de primitive e com orçamento de memória no alvo handheld; +- caminhos de spans/linhas/clears são desejáveis como aceleração interna, mas sem reabrir o modelo operacional do pipeline do jogo. + +## Problema + +Precisamos decidir qual é o modelo canônico para primitives e texto no pipeline pós-`FrameComposer`. + +Sem isso: + +- texto e primitives continuam com comportamento dependente da ordem interna do renderer; +- o stress test e qualquer cartridge que combine `composer.*` com `gfx.*` terão resultado inconsistente; +- fica indefinido se primitives pertencem ao mundo, ao HUD, ou a um overlay final. + +## Pontos Criticos + +- `draw_text(...)` e primitives screen-space não podem depender de escrita imediata em `back`. +- Para esta thread, primitives de `gfx` devem permanecer agnósticas ao pipeline canônico de render do jogo e não devem ser mescladas semanticamente com tiles/sprites. +- A ordem de composição precisa ser explícita e estável: `scene -> sprites -> HUD -> primitives/debug overlay`, ou outra ordem formal equivalente. +- Precisamos decidir se o contrato público de `gfx.*` muda semanticamente sem mudar ABI, ou se parte dessa superfície migra para `composer.*`. +- A solução deve preservar o caminho sem scene bound. +- A implementação deve evitar contaminar a infraestrutura de `gfx` responsável por scene, sprites e HUD com estado misto de overlay/debug; se necessário, o overlay deve ter fila/fase própria. +- melhorias internas de primitive path devem continuar permitidas, desde que não mudem a semântica de overlay final e não exijam buffers extras incompatíveis com o orçamento de memória aceito. + +## Opcoes + +### Opcao 1 - Manter escrita direta em `back` + +- **Abordagem:** manter `gfx.draw_text(...)` e primitives rasterizando imediatamente. +- **Pro:** zero mudança estrutural agora. +- **Contra:** o modelo continua quebrado sempre que `render_frame()` recompõe o buffer depois. +- **Tradeoff:** só funciona de forma confiável fora do caminho canônico do `FrameComposer`. + +### Opcao 2 - Fila única de draw commands pós-scene/pós-sprite + +- **Abordagem:** transformar texto e primitives em comandos diferidos, drenados depois de `scene + sprites`. +- **Pro:** resolve o problema imediato de overlay/HUD e estabiliza o stress test. +- **Contra:** mistura HUD e primitives/debug sob o mesmo conceito, reduzindo clareza contratual mesmo quando a ordem prática for a mesma. +- **Tradeoff:** simples para V1, mas semanticamente mais fraco do que separar overlay de jogo e overlay de debug. + +### Opcao 3 - Separar HUD diferido de primitives/debug overlay final + +- **Abordagem:** tratar `gfx.draw_text(...)` e demais primitives de `gfx` como overlay/debug final, separado da composição canônica de jogo (`scene + sprites + HUD`). +- **Pro:** casa com a intenção declarada para `gfx.*`: debug, artefato rápido e instrumentação visual acima do frame do jogo. +- **Contra:** exige modelar explicitamente uma fase extra no pipeline. +- **Tradeoff:** aumenta a clareza contratual e evita mesclar primitives com o domínio de jogo. + +### Opcao 4 - Manter HUD e primitives no mesmo estágio final, mas com categorias separadas + +- **Abordagem:** drenar HUD e primitives ambos no fim do frame, porém com filas/categorias distintas e ordem formal `HUD -> primitives`. +- **Pro:** preserva implementação próxima entre caminhos similares, mantendo contrato separado. +- **Contra:** é mais custoso que a opção 3 sem entregar muito valor adicional imediato. +- **Tradeoff:** bom se já houver expectativa de HUD canônico separado no curtíssimo prazo. + +## Sugestao / Recomendacao + +Seguir com a **Opcao 3**. + +Minha recomendação é: + +- retirar a escrita direta em `back` como contrato operacional para `gfx.draw_text(...)` e demais primitives de `gfx`; +- introduzir uma fila diferida canônica de primitives/debug overlay drenada no fim do frame; +- tratar `gfx.*` primitive/text como superfície agnóstica ao pipeline de jogo e explicitamente acima da composição canônica; +- não misturar semanticamente primitives com scene/tile/sprite/HUD. +- evitar compartilhar indevidamente o mesmo mecanismo operacional de composição entre overlay/debug e os caminhos de scene/sprite/HUD, mesmo quando o backend de rasterização reutilizado for o mesmo. + +Ordem recomendada para o frame canônico: + +1. limpar/compor scene; +2. compor sprites; +3. compor HUD canônico, se existir; +4. aplicar `scene_fade`; +5. aplicar `hud_fade`; +6. drenar primitives/debug overlay de `gfx.*`. + +## Perguntas em Aberto + +- `draw_text(...)` e as demais primitives de `gfx` entram todas na mesma família de overlay final já na V1, ou começamos só com `draw_text(...)`? +- `render_no_scene_frame()` deve usar a mesma fila diferida para manter semântica idêntica com e sem scene? +- HUD canônico precisa existir explicitamente nesta mesma thread, ou pode continuar implícito/externo enquanto as primitives já migram para overlay final? +- quais fast paths internos de primitives continuam desejáveis nessa nova fase, por exemplo spans horizontais/verticais, fills e clears, sem misturar isso com a composição do jogo? +- o overlay/debug final precisa de dirtying próprio por classe de primitive ou isso pode ficar fora da primeira migração? + +## Criterio para Encerrar + +Esta agenda pode ser encerrada quando tivermos uma resposta explícita para: + +- o destino semântico de `draw_text(...)`; +- se haverá uma fila própria para primitives/debug overlay e qual a relação dela com HUD; +- a ordem canônica de composição do frame; +- o escopo exato da primeira migração implementável sem reabrir o restante do pipeline. + +## Resolucao Parcial + +Direção já aceita nesta agenda: + +- primitives e `draw_text(...)` de `gfx.*` devem ser tratadas como overlay/debug final; +- esse overlay deve ser drenado **depois** de `hud_fade`; +- scene, sprites e HUD canônico não devem ser semanticamente misturados com o overlay/debug; +- a implementação deve preservar separação operacional suficiente para que o `gfx` usado pelo pipeline do jogo não passe a depender do estado transitório de primitives/debug; +- otimizações de primitive path discutidas na `AGD-0010` continuam válidas, mas passam a operar dentro do domínio de overlay/debug final, não como parte da composição canônica de scene/sprite/HUD. + +## Resolucao + +Esta agenda fica aceita com os seguintes pontos fechados: + +- `gfx.draw_text(...)` e as demais primitives públicas de `gfx.*` pertencem à mesma família V1 de overlay/debug final; +- esse overlay/debug fica **fora** do `FrameComposer`; +- `FrameComposer` continua restrito à composição canônica do jogo (`scene`, `sprites` e HUD canônico quando existir); +- o overlay/debug deve ser drenado depois de `hud_fade`; +- o caminho sem scene bound deve observar a mesma semântica final de overlay/debug; +- HUD canônico explícito não faz parte desta thread e pode permanecer implícito/externo por enquanto; +- fast paths internos de primitives continuam permitidos, desde que preservem a semântica observável do overlay/debug final; +- dirtying granular ou otimizações finas por classe de primitive não fazem parte da primeira migração normativa desta thread. diff --git a/discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md b/discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md new file mode 100644 index 00000000..be739372 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md @@ -0,0 +1,150 @@ +--- +id: DEC-0016 +ticket: deferred-overlay-and-primitive-composition +title: Deferred GFX Overlay Outside FrameComposer +status: accepted +created: 2026-04-18 +accepted: 2026-04-18 +agenda: AGD-0028 +plans: [PLN-0026, PLN-0027, PLN-0028, PLN-0029] +tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud] +--- + +## Status + +Accepted. + +## Contexto + +`DEC-0014` and `DEC-0015` established `FrameComposer` as the canonical orchestration path for game-frame composition and exposed that orchestration publicly through `composer.*`. + +That migration left `gfx.draw_text(...)` and other `gfx` primitives with their historical immediate-write behavior against the working framebuffer. Once the runtime moved to end-of-frame composition through `FrameComposer.render_frame()`, those immediate writes became unstable: scene-backed frame composition can rebuild the backbuffer after primitive calls have already touched it. + +The resulting conflict is not about whether primitives should remain available. It is about their semantic place in the pipeline. The accepted direction of this thread is that `gfx` primitives are not part of the canonical game composition model. They are primarily for debug, quick visual instrumentation, and rapid artifacts, and they must remain agnostic to scene/tile/sprite/HUD composition. + +Relevant performance context migrated from `AGD-0010` also remains in force: + +- the renderer continues to be a destructive software framebuffer model, not a retained scene graph or GPU-style renderer; +- internal primitive fast paths remain desirable; +- memory growth must remain constrained for the handheld target; +- optimization of primitive execution must not alter observable semantics. + +## Decisao + +`gfx.*` primitives and text SHALL move to a deferred final overlay model that lives outside `FrameComposer`. + +Normatively: + +- `FrameComposer` SHALL remain responsible only for canonical game-frame composition: + - scene composition; + - sprite composition; + - canonical HUD composition when such a HUD stage exists. +- `FrameComposer` MUST NOT become the owner of debug/primitive overlay state. +- Public `gfx.*` primitives, including `gfx.draw_text(...)`, SHALL belong to a V1 `gfx` overlay/debug family. +- That overlay/debug family SHALL be deferred rather than written immediately as the stable operational contract. +- The deferred overlay/debug stage SHALL be drained after `hud_fade`. +- The deferred overlay/debug stage SHALL be above scene, sprites, and canonical HUD in final visual order. +- The no-scene path MUST preserve the same final overlay/debug semantics. +- `gfx.*` primitives MUST remain semantically separate from scene/tile/sprite/HUD composition. +- The implementation MUST preserve operational separation sufficient to prevent the canonical game pipeline from depending on transient primitive/debug state. + +## Rationale + +This decision keeps the architectural boundary clean. + +`FrameComposer` exists to own the canonical game frame. Debug primitives do not belong to that contract. Pulling them into `FrameComposer` would make the orchestration service responsible for a second semantic domain with different goals: + +- game composition must be deterministic and canonical; +- primitive/text overlay must be opportunistic, screen-space, and pipeline-agnostic. + +Keeping overlay/debug outside `FrameComposer` also aligns with the stated product intent: these primitives are useful helpers, but they are not meant to become a second composition language for games. + +Draining them after `hud_fade` preserves the user-visible requirement that debug/overlay content stay truly on top and legible. This is more faithful to the accepted intent than treating primitives as part of HUD or world composition. + +Finally, separating semantic ownership still leaves room for implementation reuse. Raster backends, span paths, and buffer-writing helpers may still be shared internally, provided the public operational model remains separate. + +## Invariantes / Contrato + +### 1. Ownership Boundary + +- `FrameComposer` MUST own only canonical game-frame composition. +- Primitive/debug overlay state MUST live outside `FrameComposer`. +- The canonical game pipeline MUST NOT depend on primitive/debug overlay state for correctness. + +### 2. Overlay Semantics + +- `gfx.draw_text(...)` and sibling `gfx` primitives SHALL be treated as deferred final overlay/debug operations. +- Immediate direct writes to `back` MUST NOT remain the stable operational contract for these primitives. +- Final overlay/debug output MUST appear after: + - scene composition; + - sprite composition; + - canonical HUD composition, if present; + - `scene_fade`; + - `hud_fade`. + +### 3. Separation from Game Composition + +- Primitive/debug overlay MUST NOT be reinterpreted as scene content. +- Primitive/debug overlay MUST NOT be reinterpreted as sprite content. +- Primitive/debug overlay MUST NOT be the vehicle for canonical HUD composition. +- The public `gfx.*` primitive surface SHALL remain pipeline-agnostic relative to `composer.*`. + +### 4. Consistency Across Frame Paths + +- The scene-bound path and no-scene path MUST expose the same final overlay/debug behavior. +- Users MUST NOT need to know whether a scene is bound for `gfx.*` primitives to appear as final overlay/debug content. + +### 5. Internal Optimization Contract + +- Internal fast paths for lines, spans, fills, clears, or similar primitive operations MAY be introduced. +- Such fast paths MUST preserve the observable deferred overlay/debug semantics. +- This decision DOES NOT require fine-grained dirtying or per-primitive-class invalidation in the first migration. + +## Impactos + +### Runtime / Drivers + +- The runtime frame-end sequence must gain a distinct overlay/debug drain stage outside `FrameComposer`. +- `gfx.draw_text(...)` and peer primitives can no longer rely on stable immediate framebuffer writes once this migration lands. + +### GFX Backend + +- `Gfx` will need an explicit deferred overlay/debug command path or equivalent subsystem boundary. +- Shared raster helpers remain allowed, but the overlay/debug phase must stay semantically distinct from scene/sprite/HUD composition. + +### FrameComposer + +- `FrameComposer` must remain free of primitive/debug overlay ownership. +- Any future HUD integration must not collapse that boundary. + +### Spec / Docs + +- The canonical graphics/runtime spec must describe `gfx.*` primitives as deferred final overlay/debug operations rather than stable immediate backbuffer writes. +- Documentation that describes frame ordering must show overlay/debug after `hud_fade`. + +### Performance Follow-up + +- `AGD-0010` remains the home for broader renderer performance work, dirtying strategy, and low-level primitive optimization policy. +- Primitive optimization carried out under that thread must respect the normative separation established here. + +## Referencias + +- [AGD-0028-deferred-overlay-and-primitive-composition.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md) +- [AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md) +- [DEC-0014-frame-composer-render-integration.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md) +- [DEC-0015-frame-composer-public-syscall-surface.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md) + +## Propagacao Necessaria + +- A new implementation plan MUST be created before code changes. +- That plan MUST cover: + - deferred overlay/debug ownership outside `FrameComposer`; + - runtime frame-end ordering changes; + - no-scene path parity; + - spec/documentation updates for `gfx.*` primitive semantics. +- The implementation plan MUST NOT reopen the ownership boundary accepted here. + +## Revision Log + +- 2026-04-18: Initial accepted decision from `AGD-0028`. +- 2026-04-18: Linked implementation plan family `PLN-0026` through `PLN-0029`. diff --git a/discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md b/discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md new file mode 100644 index 00000000..ab33e3d4 --- /dev/null +++ b/discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md @@ -0,0 +1,93 @@ +--- +id: PLN-0026 +ticket: deferred-overlay-and-primitive-composition +title: Plan - GFX Overlay Contract and Spec Propagation +status: accepted +created: 2026-04-18 +completed: +tags: [gfx, runtime, spec, overlay, primitives, hud] +--- + +## Objective + +Propagate `DEC-0016` into the canonical specs and internal contracts so `gfx.*` primitives are defined as deferred final overlay/debug operations outside `FrameComposer`. + +## Background + +`DEC-0016` locks a new semantic boundary: + +- `FrameComposer` remains the owner of canonical game-frame composition; +- `gfx.*` primitives and `draw_text(...)` become deferred final overlay/debug operations; +- that overlay lives outside `FrameComposer` and is drained after `hud_fade`. + +Execution must start by updating the normative contract before implementation changes spread through runtime and drivers. + +## Scope + +### Included +- update canonical runtime/gfx spec text to describe deferred overlay semantics +- update any ABI-facing or developer-facing docs that still imply direct stable writes to `back` +- align local contract comments and module docs where they currently imply immediate-write semantics as the stable model + +### Excluded +- implementation of the overlay subsystem +- runtime frame-end integration +- final repository-wide CI + +## Execution Steps + +### Step 1 - Update canonical graphics/runtime documentation + +**What:** +Publish the new semantic contract for `gfx.*` primitives. + +**How:** +- Update the canonical runtime/gfx spec so `gfx.draw_text(...)` and peer primitives are described as deferred final overlay/debug operations. +- State explicitly that primitives are not part of canonical scene/sprite/HUD composition. +- State the ordering rule that overlay/debug is drained after `hud_fade`. +- Ensure the no-scene and scene-bound paths are described consistently. + +**File(s):** +- canonical runtime/gfx spec files under `docs/specs/runtime/` + +### Step 2 - Align implementation-facing contract text + +**What:** +Remove stale implementation comments that imply immediate stable writes to the framebuffer. + +**How:** +- Inspect module-level comments and trait docs in `hal`, `drivers`, and runtime code for language that now contradicts `DEC-0016`. +- Update only the contract-bearing comments and docs that materially affect maintenance and implementation clarity. + +**File(s):** +- `crates/console/prometeu-hal/src/gfx_bridge.rs` +- `crates/console/prometeu-drivers/src/gfx.rs` +- `crates/console/prometeu-drivers/src/frame_composer.rs` +- runtime-adjacent modules where frame ordering is described + +## Test Requirements + +### Unit Tests +- none required for pure doc propagation + +### Integration Tests +- none required for pure doc propagation + +### Manual Verification +- inspect the updated spec and local contract comments to confirm they no longer describe primitives as stable direct writes to `back` + +## Acceptance Criteria + +- [ ] Canonical spec text describes `gfx.*` primitives as deferred final overlay/debug operations. +- [ ] The spec states that overlay/debug is outside `FrameComposer`. +- [ ] The spec states that overlay/debug is drained after `hud_fade`. +- [ ] Local implementation-facing contract comments no longer imply immediate-write semantics as the stable model. + +## Dependencies + +- Source decision: `DEC-0016` + +## Risks + +- Missing a normative doc location would leave code and published contract divergent. +- Over-editing local comments could unintentionally restate design choices outside the scope of `DEC-0016`. diff --git a/discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md b/discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md new file mode 100644 index 00000000..e746e8fb --- /dev/null +++ b/discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md @@ -0,0 +1,104 @@ +--- +id: PLN-0027 +ticket: deferred-overlay-and-primitive-composition +title: Plan - Deferred GFX Overlay Subsystem +status: accepted +created: 2026-04-18 +completed: +tags: [gfx, runtime, overlay, primitives, text, drivers] +--- + +## Objective + +Introduce a dedicated deferred overlay/debug subsystem for `gfx.*` primitives outside `FrameComposer`, with command capture for `draw_text(...)` and the primitive family selected for the first migration. + +## Background + +`DEC-0016` requires primitive/text overlay ownership to remain outside `FrameComposer` while still allowing shared raster helpers and low-level optimizations internally. The new subsystem must preserve semantic separation from scene/sprite/HUD composition. + +## Scope + +### Included +- introduce an overlay/debug command queue or equivalent subsystem outside `FrameComposer` +- route `gfx.draw_text(...)` into deferred command capture instead of stable direct framebuffer writes +- route the chosen V1 primitive family into the same deferred overlay/debug path +- keep raster helper reuse allowed without merging semantic ownership + +### Excluded +- runtime frame-end sequencing +- no-scene/scene parity tests at the runtime level +- final repository-wide CI + +## Execution Steps + +### Step 1 - Define overlay/debug state ownership in drivers + +**What:** +Create the subsystem that owns deferred `gfx.*` overlay/debug commands. + +**How:** +- Add a dedicated owner adjacent to `Gfx`/`Hardware`, but not inside `FrameComposer`. +- Define the minimal command model required for V1 operations. +- Keep the subsystem screen-space and explicitly pipeline-agnostic relative to `composer.*`. + +**File(s):** +- `crates/console/prometeu-drivers/src/*` +- `crates/console/prometeu-hal/src/*` if bridge traits need extension + +### Step 2 - Route text and selected primitives into deferred capture + +**What:** +Stop treating text/primitives as stable direct writes. + +**How:** +- Change `gfx.draw_text(...)` to enqueue deferred overlay/debug work. +- Migrate the selected V1 primitive set into the same deferred path. +- Keep any remaining unmigrated primitives either explicitly out of scope or routed consistently if they are already part of the accepted V1 set. +- Preserve internal raster helper reuse where useful. + +**File(s):** +- `crates/console/prometeu-drivers/src/gfx.rs` +- runtime dispatch call sites that submit `gfx.*` primitives + +### Step 3 - Add local driver-level tests for deferred capture semantics + +**What:** +Prove that overlay/debug commands are captured separately from game composition state. + +**How:** +- Add tests that assert text/primitives do not need direct stable writes to `back` to survive until overlay drain. +- Add tests that assert the overlay owner is independent from `FrameComposer` state. + +**File(s):** +- `crates/console/prometeu-drivers/src/gfx.rs` +- new or existing driver test modules + +## Test Requirements + +### Unit Tests +- command capture tests for `draw_text(...)` +- tests for each migrated V1 primitive class +- tests proving overlay/debug state is owned outside `FrameComposer` + +### Integration Tests +- none in this plan; runtime-level ordering is covered by the next plan + +### Manual Verification +- inspect driver ownership boundaries to confirm `FrameComposer` does not gain overlay/debug state + +## Acceptance Criteria + +- [ ] A dedicated deferred overlay/debug subsystem exists outside `FrameComposer`. +- [ ] `gfx.draw_text(...)` is captured as deferred overlay/debug work. +- [ ] The selected V1 primitive family is captured through the same subsystem. +- [ ] Driver-level tests prove overlay/debug state is operationally separate from canonical game composition state. + +## Dependencies + +- Source decision: `DEC-0016` +- Prefer to execute after `PLN-0026` + +## Risks + +- Accidentally reusing `FrameComposer` storage or state would violate the accepted ownership boundary. +- Migrating only part of the primitive family without explicit scoping could create inconsistent semantics across `gfx.*`. diff --git a/discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md b/discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md new file mode 100644 index 00000000..3fadea8e --- /dev/null +++ b/discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md @@ -0,0 +1,106 @@ +--- +id: PLN-0028 +ticket: deferred-overlay-and-primitive-composition +title: Plan - Runtime Frame-End Overlay Integration and Parity +status: accepted +created: 2026-04-18 +completed: +tags: [runtime, overlay, frame-composer, no-scene, regression, stress] +--- + +## Objective + +Integrate deferred overlay/debug draining into the runtime frame-end sequence so scene-bound and no-scene frames both present the same final `gfx.*` primitive behavior after `hud_fade`. + +## Background + +After `PLN-0027`, the overlay/debug subsystem will exist but still needs to be drained in the correct place relative to `FrameComposer.render_frame()`, fades, and present/present-adjacent behavior. This plan closes the observable runtime semantics required by `DEC-0016`. + +## Scope + +### Included +- runtime frame-end ordering changes +- scene-bound and no-scene parity +- regression coverage for overlay visibility above the canonical game frame +- stress-cartridge adjustments if needed to prove text/primitives now survive frame composition + +### Excluded +- broad renderer optimization work +- final repository-wide CI + +## Execution Steps + +### Step 1 - Insert overlay/debug drain into the frame-end path + +**What:** +Drain deferred overlay/debug after canonical game composition is complete. + +**How:** +- Update the runtime frame-end path so overlay/debug drain occurs after: + - `FrameComposer.render_frame()` + - `scene_fade` + - `hud_fade` +- Ensure the same ordering is respected in the no-scene path. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs` +- `crates/console/prometeu-drivers/src/hardware.rs` +- `crates/console/prometeu-drivers/src/gfx.rs` +- any bridge traits needed by the runtime/hardware path + +### Step 2 - Add runtime and driver regressions for final visual ordering + +**What:** +Lock the new visible behavior. + +**How:** +- Add tests proving `gfx.draw_text(...)` remains visible after scene-backed frame composition. +- Add tests proving the same behavior with no scene bound. +- Add tests proving overlay/debug sits above the canonical game frame rather than being erased by it. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs` +- driver-level render tests where helpful + +### Step 3 - Update stress/integration fixtures if needed + +**What:** +Restore or improve stress scenarios that rely on visible text/primitives. + +**How:** +- Update `pbxgen-stress` or related stress fixtures so text/primitives are once again a valid visible overlay signal. +- Keep the stress focused on the new model rather than reintroducing obsolete immediate-write assumptions. + +**File(s):** +- `crates/tools/pbxgen-stress/src/lib.rs` +- `test-cartridges/stress-console/*` + +## Test Requirements + +### Unit Tests +- local ordering tests where runtime integration depends on helper sequencing + +### Integration Tests +- runtime tests for scene-bound overlay/debug visibility +- runtime tests for no-scene parity +- stress/tooling validation that text or primitives are visible again as final overlay/debug + +### Manual Verification +- run the stress path and visually confirm overlay/debug survives on top of scene/sprites after frame composition + +## Acceptance Criteria + +- [ ] The runtime drains deferred overlay/debug after canonical game composition and after `hud_fade`. +- [ ] Scene-bound and no-scene paths expose the same overlay/debug semantics. +- [ ] Regression tests prove `draw_text(...)` is no longer erased by scene-backed frame composition. +- [ ] Stress/integration fixtures reflect the new final-overlay semantics where applicable. + +## Dependencies + +- Source decision: `DEC-0016` +- Depends on `PLN-0027` + +## Risks + +- If fades are still applied after overlay/debug drain, the visible contract will contradict `DEC-0016`. +- Incomplete parity between scene-bound and no-scene paths would leave runtime behavior mode-dependent. diff --git a/discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md b/discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md new file mode 100644 index 00000000..085d83c2 --- /dev/null +++ b/discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md @@ -0,0 +1,82 @@ +--- +id: PLN-0029 +ticket: deferred-overlay-and-primitive-composition +title: Plan - Final Overlay CI Validation and Polish +status: accepted +created: 2026-04-18 +completed: +tags: [ci, overlay, runtime, gfx, validation] +--- + +## Objective + +Run the final repository validation path for the deferred overlay/debug migration and perform the last compatibility, formatting, lint, and regression fixes required to close the thread cleanly. + +## Background + +`DEC-0016` changes visible runtime semantics and touches both specs and code paths around frame composition. A dedicated final-validation plan is needed so the implementation family can close on a clean CI signal rather than leaving integration fallout for later. + +## Scope + +### Included +- full-tree formatting, lint, and test validation +- stress-path smoke validation after overlay integration +- final cleanup fixes required to satisfy CI + +### Excluded +- new feature work outside the accepted overlay/debug migration + +## Execution Steps + +### Step 1 - Run focused validation before full CI + +**What:** +Catch local fallout in the touched areas before the full repository pass. + +**How:** +- Run targeted tests for drivers, runtime, and `pbxgen-stress`. +- Inspect touched files for stale immediate-write assumptions or missed contract updates. + +**File(s):** +- touched files from `PLN-0026` through `PLN-0028` + +### Step 2 - Run final repository CI + +**What:** +Validate the migration end to end. + +**How:** +- Run the repository validation path, including `make ci`. +- Fix any final formatting, lint, test, or generated-fixture fallout caused by the overlay/debug migration. +- Do not widen scope beyond the accepted thread. + +**File(s):** +- repository-wide + +## Test Requirements + +### Unit Tests +- all relevant crate unit tests pass after the migration + +### Integration Tests +- runtime and stress/integration tests pass after the migration +- `make ci` passes + +### Manual Verification +- inspect the tree for residual direct-write assumptions or incomplete overlay propagation + +## Acceptance Criteria + +- [ ] Targeted validation passes for the touched drivers/runtime/stress areas. +- [ ] `make ci` passes after the deferred overlay/debug migration family lands. +- [ ] No residual contract mismatch remains between spec text and code behavior in the touched thread. + +## Dependencies + +- Source decision: `DEC-0016` +- Depends on `PLN-0026`, `PLN-0027`, and `PLN-0028` + +## Risks + +- Final CI may surface unrelated renderer assumptions that still expect immediate-write semantics. +- Generated cartridge fixtures may drift if regeneration is forgotten during earlier plans. diff --git a/test-cartridges/stress-console/assets.pa b/test-cartridges/stress-console/assets.pa new file mode 100644 index 0000000000000000000000000000000000000000..77c82efc858fcacad0ea558f4daa450b87705dba GIT binary patch literal 35432 zcmeI5&r1|h0LN!as0hO75Ohff3W5!E(?z9h#U#Np$si zz=?rgdE|r$+?*S@PUozX7lpHVd6xEsGwBvZ5QvOB7396Lke8Kejohp^Rg#|zt)Ng8 z_4CWarVF_sFHcaZB-AOjtTUyoDB+BTdU~tZwK}8PL~0W_kZkXeGgv#TR9mW9PIdih zRVpL$%FCkYxpEp9wO{nY++x?e>W@^m8-6USaRdB&LGEY+1LV#;7$A4u1p{Q)TQER6 zTfhLh=O7p$_f3NV^7sQVKn}hK1LUbjFhHK}0R!ahB``o+Pt6Lfe&lzns^a61UfffihIv)R$D0iO+-m6OHbXy?hJL0D{p>LG^N^vR-Tm=7 ztIiQaKl=>*>^Jn&GxXCp^z(wDpEHJj-dj;;8y{aa^mES8&ufN$E*Sdx(9qAvhJHR7 zjn7GS-Wl#s9}N9`Zs_L=Lq9(o`uX*~dbRQKw>NQ5b-r&_6^@5`26}C)p=#S!_|Yqe z!!0dGO>(PR;ooqU+?YX~0h=t41u~#zfZgD|g7->21MG%A7xKbf*bOqkbwJMmyP?m8 zyf7DbgA8yT&@;ep=yM@2%!S<`16&8l02v?yWPl9(i-Fp+0crVcKsx?xK==e9H)cQv z$biCtt&F)oaE&Ab5d-W-lm)q97Agbm26<3<;5g!e-H5Uv7tBIsfZZSuDi0h-Jg^&4 z7UY6is0^?hb0IIxh200j0WzRtp#DyQF8NM@uC$D(Np64NDG4>5 z6we_@e6MbT zx3RB5+R$IHGrvlbG)tm1tul6-gu{rS{0L;yT=A)jqlB)s)$-MHR6hXanP$+8wSe6` zPQ3O?Zp@Sdkt9tZzhn0b@3~sB7H=53@Q8GDb@VcaVEF{NjgzO7^SIC%|0v@2BqPl{ z&)&-g#uofuaYKvK&}<(od1!gUY#+E@QO(aRd!uFco1QZj801xE4v5VBOw?DMJiOCM z9&6=%|3@vpqrLMm3l7%I!o=(pyNX_xSHx|1$mfdbby?=H9H|v6pU`r0iw!fm#ey}) zie|I<@V3?C^9WGB&|uDaHQUBckzqT)m-K`dI>J60##r?SxrE0&dhYDoFzk<5Ge2wd zO;_aZc2<|(APSS<==i9lwdCHLB%B6WG|NhQrXqv!-I`33eulSC)|&Lj!^t>}l7?LO kM>j#*i$;j?w;D^MEa>4MlZ3y|ZAvukXF)nKpmiMl2G1;2DgXcg literal 945 zcmZ`%%}&BV5T5d*A~8W@EGI(<2M@-e36dBN6<$E&12hE+G*Y158t}$r_z)g_5}(48 zC-3;pvOS{F9A*3O+bx+X|1Agrzh&nL*E4$cvNGr^~s15vi5N2%aGJoruf zj>nB;=$!8%OT)pUK)tc+gm&Y$VaTo%v>!TiyYH! Date: Sat, 18 Apr 2026 09:51:30 +0100 Subject: [PATCH 16/21] implements PLN-0026 --- .../prometeu-drivers/src/frame_composer.rs | 5 +++ crates/console/prometeu-drivers/src/gfx.rs | 24 +++------- crates/console/prometeu-hal/src/gfx_bridge.rs | 12 +++++ docs/specs/runtime/04-gfx-peripheral.md | 44 ++++++++++++++++--- 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index 9a174e18..02a2f471 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -247,10 +247,15 @@ impl FrameComposer { { let update = resolver.update(scene, self.camera_x_px, self.camera_y_px); Self::apply_refresh_requests(cache, scene, &update.refresh_requests); + // `FrameComposer` owns only canonical game-frame composition. + // Deferred `gfx.*` primitives are drained later by a separate + // overlay/debug stage outside this service boundary. gfx.render_scene_from_cache(cache, &update); return; } + // No-scene frames still stop at canonical game composition. Final + // overlay/debug work remains outside `FrameComposer`. gfx.render_no_scene_frame(); } diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 47b06899..e2c2534a 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -31,22 +31,11 @@ pub enum BlendMode { /// PROMETEU Graphics Subsystem (GFX). /// -/// Models a specialized graphics chip with a fixed resolution, double buffering, -/// and a multi-layered tile/sprite architecture. -/// -/// The GFX system works by composing several "layers" into a single 16-bit -/// RGB565 framebuffer. It supports hardware-accelerated primitives (lines, rects) -/// and specialized console features like background scrolling and sprite sorting. -/// -/// ### Layer Composition Order (back to front): -/// 1. **Priority 0 Sprites**: Objects behind everything else. -/// 2. **Tile Layer 0 + Priority 1 Sprites**: Background 0. -/// 3. **Tile Layer 1 + Priority 2 Sprites**: Background 1. -/// 4. **Tile Layer 2 + Priority 3 Sprites**: Background 2. -/// 5. **Tile Layer 3 + Priority 4 Sprites**: Foreground. -/// 6. **Scene Fade**: Global brightness/color filter. -/// 7. **HUD Layer**: Fixed UI elements (always on top). -/// 8. **HUD Fade**: Independent fade for the UI. +/// `Gfx` owns the framebuffer backend and the canonical game-frame raster path +/// consumed by `FrameComposer`. That canonical path covers scene composition, +/// sprite composition, and fades. Public `gfx.*` primitives remain valid, but +/// they do not define the canonical game composition contract; they belong to a +/// separate final overlay/debug stage. pub struct Gfx { /// Width of the internal framebuffer in pixels. w: usize, @@ -54,7 +43,8 @@ pub struct Gfx { h: usize, /// Front buffer: the "VRAM" currently being displayed by the Host window. front: Vec, - /// Back buffer: the "Work RAM" where new frames are composed. + /// Back buffer: the working buffer where canonical game frames are composed + /// before any final overlay/debug drain. back: Vec, /// Shared access to graphical memory banks (tiles and palettes). diff --git a/crates/console/prometeu-hal/src/gfx_bridge.rs b/crates/console/prometeu-hal/src/gfx_bridge.rs index 44829579..dc989f82 100644 --- a/crates/console/prometeu-hal/src/gfx_bridge.rs +++ b/crates/console/prometeu-hal/src/gfx_bridge.rs @@ -48,9 +48,21 @@ pub trait GfxBridge { fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color); fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color); fn present(&mut self); + /// Render the canonical game frame with no bound scene. + /// + /// Deferred `gfx.*` overlay/debug primitives are intentionally outside this + /// contract and are drained by a separate final overlay stage. fn render_no_scene_frame(&mut self); + /// Render the canonical scene-backed game frame from cache/resolver state. + /// + /// Deferred `gfx.*` overlay/debug primitives are intentionally outside this + /// contract and are drained by a separate final overlay stage. fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate); fn load_frame_sprites(&mut self, sprites: &[Sprite]); + /// Submit text into the `gfx.*` primitive path. + /// + /// Under the accepted runtime contract this is not the canonical game + /// composition path; it belongs to the deferred final overlay/debug family. fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color); fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color); diff --git a/docs/specs/runtime/04-gfx-peripheral.md b/docs/specs/runtime/04-gfx-peripheral.md index d0e5186f..8b63b63a 100644 --- a/docs/specs/runtime/04-gfx-peripheral.md +++ b/docs/specs/runtime/04-gfx-peripheral.md @@ -48,10 +48,12 @@ The GFX maintains two buffers: Per-frame flow: -1. The system draws to the back buffer -2. Calls `present()` -3. Buffers are swapped -4. The host displays the front buffer +1. The system prepares the logical frame +2. Canonical game composition is rendered into the back buffer +3. Deferred final overlay/debug primitives are drained on top of the completed game frame +4. Calls `present()` +5. Buffers are swapped +6. The host displays the front buffer This guarantees: @@ -183,7 +185,7 @@ Access: --- -## 10. Projection to the Back Buffer +## 10. Canonical Game Projection to the Back Buffer For each frame: @@ -198,6 +200,10 @@ For each frame: 3. Draw HUD layer last +This section describes only the canonical game composition path. + +`gfx.*` primitives such as `draw_text`, `draw_line`, and `draw_disc` are not part of this canonical game projection order. In v1 they belong to a deferred final overlay/debug stage that is drained after canonical game composition is complete. + --- ## 11. Drawing Order and Priority @@ -214,6 +220,15 @@ Base order: 4. Tile Layer 3 5. Sprites (by priority between layers) 6. HUD Layer +7. Scene Fade +8. HUD Fade +9. Deferred `gfx.*` overlay/debug primitives + +Normative boundary: + +- Items 1 through 8 belong to canonical game-frame composition. +- Item 9 is a separate overlay/debug stage. +- Deferred `gfx.*` primitives MUST NOT be interpreted as scene, sprite, or canonical HUD content. --- @@ -258,8 +273,9 @@ Everything is: ## 14. Where Blend is Applied - Blending occurs during drawing -- The result goes directly to the back buffer -- There is no automatic post-composition +- For canonical game composition, the result goes to the back buffer during composition +- For deferred `gfx.*` overlay/debug primitives, the result is applied during the final overlay/debug drain stage +- There is no automatic GPU-style post-processing pipeline --- @@ -296,6 +312,8 @@ controls: - **Scene Fade**: affects the entire scene (Tile Layers 0–3 + Sprites) - **HUD Fade**: affects only the HUD Layer (always composed last) +In v1, deferred `gfx.*` overlay/debug primitives are drained after both fades and therefore are not themselves part of scene or HUD fade application. + The fade is implemented without continuous per-pixel alpha and without floats. It uses a **discrete integer level** (0..31), which in practice produces an "almost continuous" visual result in 320×180 pixel art. @@ -566,6 +584,18 @@ Fault boundary: | `composer.set_camera` | `void` | no real operational failure path in v1 | | `composer.emit_sprite` | `status:int` | explicit orchestration-domain operational rejection | +### 19.1.a Deferred overlay/debug semantics for `gfx.*` + +The public `gfx.*` primitive family remains valid in v1, but its stable operational meaning is: + +- deferred final overlay/debug composition; +- screen-space and pipeline-agnostic relative to `composer.*`; +- outside `FrameComposer`; +- above scene, sprites, and canonical HUD; +- drained after `hud_fade`. + +This means callers MUST NOT rely on stable immediate writes to the working back buffer as the public contract for `gfx.draw_text(...)` or sibling primitives. + ### 19.2 `composer.emit_sprite` `composer.emit_sprite` returns `status:int`. -- 2.47.2 From 2865cb3803fc2e84be95d6eed24278c9ace3f21c Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 18 Apr 2026 09:55:15 +0100 Subject: [PATCH 17/21] implements PLN-0027 --- crates/console/prometeu-drivers/src/gfx.rs | 68 ++++++++++++++++++- .../prometeu-drivers/src/gfx_overlay.rs | 35 ++++++++++ .../console/prometeu-drivers/src/hardware.rs | 1 + crates/console/prometeu-drivers/src/lib.rs | 2 + 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 crates/console/prometeu-drivers/src/gfx_overlay.rs diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index e2c2534a..56b6814d 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -1,3 +1,4 @@ +use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand}; use crate::memory_banks::GlyphBankPoolAccess; use prometeu_hal::GfxBridge; use prometeu_hal::color::Color; @@ -49,6 +50,8 @@ pub struct Gfx { /// Shared access to graphical memory banks (tiles and palettes). pub glyph_banks: Arc, + /// Deferred overlay/debug capture kept separate from canonical game composition. + overlay: DeferredGfxOverlay, /// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory). pub sprites: [Sprite; 512], @@ -288,6 +291,7 @@ impl Gfx { front: vec![0; len], back: vec![0; len], glyph_banks, + overlay: DeferredGfxOverlay::default(), sprites: [EMPTY_SPRITE; 512], sprite_count: 0, scene_fade_level: 31, @@ -307,6 +311,14 @@ impl Gfx { (self.w, self.h) } + pub fn begin_overlay_frame(&mut self) { + self.overlay.begin_frame(); + } + + pub fn overlay(&self) -> &DeferredGfxOverlay { + &self.overlay + } + /// The buffer that the host should display (RGB565). pub fn front_buffer(&self) -> &[u16] { &self.front @@ -326,6 +338,7 @@ impl Gfx { color: Color, mode: BlendMode, ) { + self.overlay.push(OverlayCommand::FillRectBlend { x, y, w, h, color, mode }); if color == Color::COLOR_KEY { return; } @@ -367,6 +380,7 @@ impl Gfx { /// Draws a line between two points using Bresenham's algorithm. pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) { + self.overlay.push(OverlayCommand::DrawLine { x0, y0, x1, y1, color }); if color == Color::COLOR_KEY { return; } @@ -399,6 +413,7 @@ impl Gfx { /// Draws a circle outline using Midpoint Circle Algorithm. pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { + self.overlay.push(OverlayCommand::DrawCircle { x: xc, y: yc, r, color }); if color == Color::COLOR_KEY { return; } @@ -467,6 +482,7 @@ impl Gfx { /// Draws a disc (filled circle with border). pub fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) { + self.overlay.push(OverlayCommand::DrawDisc { x, y, r, border_color, fill_color }); self.fill_circle(x, y, r, fill_color); self.draw_circle(x, y, r, border_color); } @@ -496,6 +512,7 @@ impl Gfx { border_color: Color, fill_color: Color, ) { + self.overlay.push(OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color }); self.fill_rect(x, y, w, h, fill_color); self.draw_rect(x, y, w, h, border_color); } @@ -783,6 +800,7 @@ impl Gfx { } pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { + self.overlay.push(OverlayCommand::DrawText { x, y, text: text.to_string(), color }); let mut cx = x; for c in text.chars() { self.draw_char(cx, y, c, color); @@ -827,7 +845,8 @@ impl Gfx { #[cfg(test)] mod tests { use super::*; - use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks}; + use crate::FrameComposer; + use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess}; use prometeu_hal::glyph_bank::TileSize; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; @@ -926,9 +945,14 @@ mod tests { fn test_draw_line() { let banks = Arc::new(MemoryBanks::new()); let mut gfx = Gfx::new(10, 10, banks); + gfx.begin_overlay_frame(); gfx.draw_line(0, 0, 9, 9, Color::WHITE); assert_eq!(gfx.back[0], Color::WHITE.0); assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0); + assert_eq!( + gfx.overlay().commands(), + &[OverlayCommand::DrawLine { x0: 0, y0: 0, x1: 9, y1: 9, color: Color::WHITE }] + ); } #[test] @@ -954,11 +978,53 @@ mod tests { fn test_draw_square() { let banks = Arc::new(MemoryBanks::new()); let mut gfx = Gfx::new(10, 10, banks); + gfx.begin_overlay_frame(); gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK); // Border assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0); // Fill assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0); + assert_eq!(gfx.overlay().command_count(), 2); + } + + #[test] + fn draw_text_captures_overlay_command() { + let banks = Arc::new(MemoryBanks::new()); + let mut gfx = Gfx::new(32, 18, banks); + gfx.begin_overlay_frame(); + + gfx.draw_text(4, 5, "HUD", Color::WHITE); + + assert_eq!( + gfx.overlay().commands(), + &[OverlayCommand::DrawText { x: 4, y: 5, text: "HUD".into(), color: Color::WHITE }] + ); + } + + #[test] + fn overlay_state_is_separate_from_frame_composer_sprite_state() { + let banks = Arc::new(MemoryBanks::new()); + let mut gfx = Gfx::new(32, 18, Arc::clone(&banks) as Arc); + let mut frame_composer = + FrameComposer::new(32, 18, Arc::clone(&banks) as Arc); + + gfx.begin_overlay_frame(); + frame_composer.begin_frame(); + frame_composer.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 0, palette_id: 0 }, + x: 1, + y: 2, + layer: 0, + bank_id: 0, + active: true, + flip_x: false, + flip_y: false, + priority: 0, + }); + gfx.draw_text(1, 1, "X", Color::WHITE); + + assert_eq!(frame_composer.sprite_controller().sprite_count(), 1); + assert_eq!(gfx.overlay().command_count(), 1); } #[test] diff --git a/crates/console/prometeu-drivers/src/gfx_overlay.rs b/crates/console/prometeu-drivers/src/gfx_overlay.rs new file mode 100644 index 00000000..6b9641ad --- /dev/null +++ b/crates/console/prometeu-drivers/src/gfx_overlay.rs @@ -0,0 +1,35 @@ +use crate::gfx::BlendMode; +use prometeu_hal::color::Color; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OverlayCommand { + FillRectBlend { x: i32, y: i32, w: i32, h: i32, color: Color, mode: BlendMode }, + DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color }, + DrawCircle { x: i32, y: i32, r: i32, color: Color }, + DrawDisc { x: i32, y: i32, r: i32, border_color: Color, fill_color: Color }, + DrawSquare { x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color }, + DrawText { x: i32, y: i32, text: String, color: Color }, +} + +#[derive(Debug, Clone, Default)] +pub struct DeferredGfxOverlay { + commands: Vec, +} + +impl DeferredGfxOverlay { + pub fn begin_frame(&mut self) { + self.commands.clear(); + } + + pub fn push(&mut self, command: OverlayCommand) { + self.commands.push(command); + } + + pub fn commands(&self) -> &[OverlayCommand] { + &self.commands + } + + pub fn command_count(&self) -> usize { + self.commands.len() + } +} diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index d648356b..42d02f34 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -48,6 +48,7 @@ impl Default for Hardware { impl HardwareBridge for Hardware { fn begin_frame(&mut self) { + self.gfx.begin_overlay_frame(); self.frame_composer.begin_frame(); } diff --git a/crates/console/prometeu-drivers/src/lib.rs b/crates/console/prometeu-drivers/src/lib.rs index 9a088424..397e9a07 100644 --- a/crates/console/prometeu-drivers/src/lib.rs +++ b/crates/console/prometeu-drivers/src/lib.rs @@ -2,6 +2,7 @@ mod asset; mod audio; mod frame_composer; mod gfx; +mod gfx_overlay; pub mod hardware; mod memory_banks; mod pad; @@ -11,6 +12,7 @@ pub use crate::asset::AssetManager; pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; pub use crate::frame_composer::{FrameComposer, SceneStatus, SpriteController}; pub use crate::gfx::Gfx; +pub use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand}; pub use crate::memory_banks::{ GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess, SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller, -- 2.47.2 From aaed1e95dd4e238480e16ec51130bb26c423b863 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 18 Apr 2026 09:56:50 +0100 Subject: [PATCH 18/21] implements PLN-0028 --- crates/console/prometeu-drivers/src/gfx.rs | 70 ++++++++++++--- .../prometeu-drivers/src/gfx_overlay.rs | 4 + .../console/prometeu-drivers/src/hardware.rs | 1 + .../src/virtual_machine_runtime/tests.rs | 89 +++++++++++++++++++ 4 files changed, 153 insertions(+), 11 deletions(-) diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 56b6814d..a39e0658 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -52,6 +52,8 @@ pub struct Gfx { pub glyph_banks: Arc, /// Deferred overlay/debug capture kept separate from canonical game composition. overlay: DeferredGfxOverlay, + /// Internal guard to replay deferred overlay commands without re-enqueueing them. + is_draining_overlay: bool, /// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory). pub sprites: [Sprite; 512], @@ -292,6 +294,7 @@ impl Gfx { back: vec![0; len], glyph_banks, overlay: DeferredGfxOverlay::default(), + is_draining_overlay: false, sprites: [EMPTY_SPRITE; 512], sprite_count: 0, scene_fade_level: 31, @@ -319,6 +322,34 @@ impl Gfx { &self.overlay } + pub fn drain_overlay_debug(&mut self) { + let commands = self.overlay.take_commands(); + self.is_draining_overlay = true; + + for command in commands { + match command { + OverlayCommand::FillRectBlend { x, y, w, h, color, mode } => { + self.fill_rect_blend(x, y, w, h, color, mode) + } + OverlayCommand::DrawLine { x0, y0, x1, y1, color } => { + self.draw_line(x0, y0, x1, y1, color) + } + OverlayCommand::DrawCircle { x, y, r, color } => self.draw_circle(x, y, r, color), + OverlayCommand::DrawDisc { x, y, r, border_color, fill_color } => { + self.draw_disc(x, y, r, border_color, fill_color) + } + OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color } => { + self.draw_square(x, y, w, h, border_color, fill_color) + } + OverlayCommand::DrawText { x, y, text, color } => { + self.draw_text(x, y, &text, color) + } + } + } + + self.is_draining_overlay = false; + } + /// The buffer that the host should display (RGB565). pub fn front_buffer(&self) -> &[u16] { &self.front @@ -338,7 +369,10 @@ impl Gfx { color: Color, mode: BlendMode, ) { - self.overlay.push(OverlayCommand::FillRectBlend { x, y, w, h, color, mode }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::FillRectBlend { x, y, w, h, color, mode }); + return; + } if color == Color::COLOR_KEY { return; } @@ -380,7 +414,10 @@ impl Gfx { /// Draws a line between two points using Bresenham's algorithm. pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) { - self.overlay.push(OverlayCommand::DrawLine { x0, y0, x1, y1, color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawLine { x0, y0, x1, y1, color }); + return; + } if color == Color::COLOR_KEY { return; } @@ -413,7 +450,10 @@ impl Gfx { /// Draws a circle outline using Midpoint Circle Algorithm. pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { - self.overlay.push(OverlayCommand::DrawCircle { x: xc, y: yc, r, color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawCircle { x: xc, y: yc, r, color }); + return; + } if color == Color::COLOR_KEY { return; } @@ -482,7 +522,10 @@ impl Gfx { /// Draws a disc (filled circle with border). pub fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) { - self.overlay.push(OverlayCommand::DrawDisc { x, y, r, border_color, fill_color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawDisc { x, y, r, border_color, fill_color }); + return; + } self.fill_circle(x, y, r, fill_color); self.draw_circle(x, y, r, border_color); } @@ -512,7 +555,10 @@ impl Gfx { border_color: Color, fill_color: Color, ) { - self.overlay.push(OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color }); + return; + } self.fill_rect(x, y, w, h, fill_color); self.draw_rect(x, y, w, h, border_color); } @@ -800,7 +846,10 @@ impl Gfx { } pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { - self.overlay.push(OverlayCommand::DrawText { x, y, text: text.to_string(), color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawText { x, y, text: text.to_string(), color }); + return; + } let mut cx = x; for c in text.chars() { self.draw_char(cx, y, c, color); @@ -947,12 +996,10 @@ mod tests { let mut gfx = Gfx::new(10, 10, banks); gfx.begin_overlay_frame(); gfx.draw_line(0, 0, 9, 9, Color::WHITE); + gfx.drain_overlay_debug(); assert_eq!(gfx.back[0], Color::WHITE.0); assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0); - assert_eq!( - gfx.overlay().commands(), - &[OverlayCommand::DrawLine { x0: 0, y0: 0, x1: 9, y1: 9, color: Color::WHITE }] - ); + assert_eq!(gfx.overlay().command_count(), 0); } #[test] @@ -980,11 +1027,12 @@ mod tests { let mut gfx = Gfx::new(10, 10, banks); gfx.begin_overlay_frame(); gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK); + gfx.drain_overlay_debug(); // Border assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0); // Fill assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0); - assert_eq!(gfx.overlay().command_count(), 2); + assert_eq!(gfx.overlay().command_count(), 0); } #[test] diff --git a/crates/console/prometeu-drivers/src/gfx_overlay.rs b/crates/console/prometeu-drivers/src/gfx_overlay.rs index 6b9641ad..be77e9c4 100644 --- a/crates/console/prometeu-drivers/src/gfx_overlay.rs +++ b/crates/console/prometeu-drivers/src/gfx_overlay.rs @@ -32,4 +32,8 @@ impl DeferredGfxOverlay { pub fn command_count(&self) -> usize { self.commands.len() } + + pub fn take_commands(&mut self) -> Vec { + std::mem::take(&mut self.commands) + } } diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 42d02f34..a7f09aec 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -70,6 +70,7 @@ impl HardwareBridge for Hardware { fn render_frame(&mut self) { self.frame_composer.render_frame(&mut self.gfx); + self.gfx.drain_overlay_debug(); } fn has_glyph_bank(&self, bank_id: usize) -> bool { diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index 68780ef9..6ca08d2b 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -354,6 +354,95 @@ fn tick_renders_scene_through_public_composer_syscalls() { assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); } +#[test] +fn tick_draw_text_survives_scene_backed_frame_composition() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let signals = InputSignals::default(); + let code = assemble( + "PUSH_I32 0\nHOSTCALL 0\nPOP_N 1\n\ + PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 1\n\ + PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 65535\nHOSTCALL 2\n\ + FRAME_SYNC\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("I".into())], + vec![ + SyscallDecl { + module: "composer".into(), + name: "bind_scene".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }, + SyscallDecl { + module: "composer".into(), + name: "set_camera".into(), + version: 1, + arg_slots: 2, + ret_slots: 0, + }, + SyscallDecl { + module: "gfx".into(), + name: "draw_text".into(), + version: 1, + arg_slots: 4, + ret_slots: 0, + }, + ], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE))); + banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8))); + let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks)); + hardware.gfx.scene_fade_level = 31; + hardware.gfx.hud_fade_level = 31; + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick(&mut vm, &signals, &mut hardware); + + assert!(report.is_none(), "scene-backed overlay text must not crash"); + hardware.gfx.present(); + assert_eq!(hardware.gfx.front_buffer()[0], Color::WHITE.raw()); +} + +#[test] +fn tick_draw_text_survives_no_scene_frame_path() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let signals = InputSignals::default(); + let code = assemble( + "PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 65535\nHOSTCALL 0\nFRAME_SYNC\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("I".into())], + vec![SyscallDecl { + module: "gfx".into(), + name: "draw_text".into(), + version: 1, + arg_slots: 4, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + let mut hardware = Hardware::new(); + hardware.gfx.scene_fade_level = 31; + hardware.gfx.hud_fade_level = 31; + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick(&mut vm, &signals, &mut hardware); + + assert!(report.is_none(), "no-scene overlay text must not crash"); + hardware.gfx.present(); + assert_eq!(hardware.gfx.front_buffer()[0], Color::WHITE.raw()); +} + #[test] fn initialize_vm_success_clears_previous_crash_report() { let mut runtime = VirtualMachineRuntime::new(None); -- 2.47.2 From 1c74631a5e7a5f41b646dd4342282dbb2c2648c0 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 18 Apr 2026 09:57:49 +0100 Subject: [PATCH 19/21] implements PLN-0029 --- crates/tools/pbxgen-stress/src/lib.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index 2600c12d..8999c6c9 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -175,20 +175,16 @@ fn heavy_load(rom: &mut Vec) { rom.extend(asm("JMP 0")); let row_loop_end = rom.len() as u32; - rom.extend(asm( - "PUSH_I32 8\n\ + rom.extend(asm("PUSH_I32 8\n\ PUSH_I32 8\n\ PUSH_CONST 0\n\ GET_GLOBAL 0\nPUSH_I32 2047\nMUL\nPUSH_I32 65535\nBIT_AND\n\ - HOSTCALL 1", - )); - rom.extend(asm( - "PUSH_I32 8\n\ + HOSTCALL 1")); + rom.extend(asm("PUSH_I32 8\n\ PUSH_I32 20\n\ PUSH_CONST 1\n\ GET_GLOBAL 0\nPUSH_I32 4093\nMUL\nPUSH_I32 65535\nBIT_AND\n\ - HOSTCALL 1", - )); + HOSTCALL 1")); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ")); let jif_log_offset = rom.len() + 2; -- 2.47.2 From 76254928e68f7bd6fc3a6dc4fc953a3984efca7b Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 18 Apr 2026 16:24:20 +0100 Subject: [PATCH 20/21] stress test cart fixes --- crates/tools/pbxgen-stress/src/lib.rs | 18 +++++++++++++++--- test-cartridges/stress-console/program.pbx | Bin 984 -> 1205 bytes 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index 8999c6c9..7f165b8c 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -92,6 +92,8 @@ pub fn generate() -> Result<()> { const_pool: vec![ ConstantPoolEntry::String("stress".into()), ConstantPoolEntry::String("frame".into()), + ConstantPoolEntry::String("overlay".into()), + ConstantPoolEntry::String("composer".into()), ], functions, code: rom, @@ -175,16 +177,26 @@ fn heavy_load(rom: &mut Vec) { rom.extend(asm("JMP 0")); let row_loop_end = rom.len() as u32; - rom.extend(asm("PUSH_I32 8\n\ + rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 220\nMOD\n\ PUSH_I32 8\n\ PUSH_CONST 0\n\ GET_GLOBAL 0\nPUSH_I32 2047\nMUL\nPUSH_I32 65535\nBIT_AND\n\ HOSTCALL 1")); - rom.extend(asm("PUSH_I32 8\n\ - PUSH_I32 20\n\ + rom.extend(asm("PUSH_I32 12\n\ + GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 120\nMOD\nPUSH_I32 24\nADD\n\ PUSH_CONST 1\n\ GET_GLOBAL 0\nPUSH_I32 4093\nMUL\nPUSH_I32 65535\nBIT_AND\n\ HOSTCALL 1")); + rom.extend(asm("PUSH_I32 220\n\ + GET_GLOBAL 0\nPUSH_I32 5\nMUL\nPUSH_I32 140\nMOD\n\ + PUSH_CONST 2\n\ + GET_GLOBAL 0\nPUSH_I32 1237\nMUL\nPUSH_I32 65535\nBIT_AND\n\ + HOSTCALL 1")); + rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 4\nMUL\nPUSH_I32 180\nMOD\nPUSH_I32 80\nADD\n\ + GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 90\nMOD\nPUSH_I32 70\nADD\n\ + PUSH_CONST 3\n\ + GET_GLOBAL 0\nPUSH_I32 3001\nMUL\nPUSH_I32 65535\nBIT_AND\n\ + HOSTCALL 1")); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ")); let jif_log_offset = rom.len() + 2; diff --git a/test-cartridges/stress-console/program.pbx b/test-cartridges/stress-console/program.pbx index c20885dc8d5920cc7392278538a867837f964839..5f278669fb6c02e025ae114d9ed1ffc186e878e7 100644 GIT binary patch delta 353 zcmcb?zLj%=M!gXO0|O%v&jMl*AZ7yMbwKRH%)kId48MQ`NSpe=n0$gsxgMg!0muib z1GxaCt_Mh~0No4H=K#bYxvM}|gXI4IXJ9aACB< eMnPrWz_J1$H*>=k?c|<(lSzxogLyJ5^JM^w$S9Hk delta 173 zcmdnWd4qj|M!h5h0|O%vHv+K;5HkVsR3PpEGMIt1ERX<+vjDLnkSzeDS%KIQ$es?Q zLF!r9fJAXgQEG893rH@lC^0t`WGD!L41y?AU~m928N|UBDlj+#*&7eMU}P0%;9y{w eyq8Iv6G#K4f%Z-2WLBPhf?0y8g=z95=F0#mz!h)+ -- 2.47.2 From 73cf96ed6c62635ffca24863dd3e83606834d468 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 18 Apr 2026 16:31:23 +0100 Subject: [PATCH 21/21] housekeep --- .../.backups/index.ndjson.20260418-162736.bak | 29 ++ .../.backups/index.ndjson.20260418-162925.bak | 29 ++ discussion/index.ndjson | 8 +- ...sition-belongs-above-the-render-backend.md | 74 ++++ ...t-follow-the-canonical-service-boundary.md | 56 +++ ...al-overlay-not-part-of-game-composition.md | 56 +++ ...-all-scene-cache-and-camera-integration.md | 405 ------------------ ...7-frame-composer-public-syscall-surface.md | 214 --------- ...erred-overlay-and-primitive-composition.md | 140 ------ ...-0014-frame-composer-render-integration.md | 205 --------- ...5-frame-composer-public-syscall-surface.md | 166 ------- ...rred-gfx-overlay-outside-frame-composer.md | 150 ------- ...me-composer-core-and-hardware-ownership.md | 121 ------ ...ite-controller-and-frame-emission-model.md | 127 ------ ...9-scene-binding-camera-and-scene-status.md | 135 ------ ...020-cache-refresh-and-render-frame-path.md | 136 ------ ...ement-callsite-migration-and-regression.md | 123 ------ ...ser-syscall-domain-and-spec-propagation.md | 122 ------ ...ser-runtime-dispatch-and-legacy-removal.md | 112 ----- ...tridge-tooling-and-regression-migration.md | 107 ----- ...PLN-0025-final-ci-validation-and-polish.md | 96 ----- ...x-overlay-contract-and-spec-propagation.md | 93 ---- ...PLN-0027-deferred-gfx-overlay-subsystem.md | 104 ----- ...rame-end-overlay-integration-and-parity.md | 106 ----- ...-final-overlay-ci-validation-and-polish.md | 82 ---- 25 files changed, 248 insertions(+), 2748 deletions(-) create mode 100644 discussion/.backups/index.ndjson.20260418-162736.bak create mode 100644 discussion/.backups/index.ndjson.20260418-162925.bak create mode 100644 discussion/lessons/DSC-0026-render-all-scene-cache-and-camera-integration/LSN-0031-frame-composition-belongs-above-the-render-backend.md create mode 100644 discussion/lessons/DSC-0027-frame-composer-public-syscall-surface/LSN-0032-public-abi-must-follow-the-canonical-service-boundary.md create mode 100644 discussion/lessons/DSC-0028-deferred-overlay-and-primitive-composition/LSN-0033-debug-primitives-should-be-a-final-overlay-not-part-of-game-composition.md delete mode 100644 discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md delete mode 100644 discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md delete mode 100644 discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md delete mode 100644 discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md delete mode 100644 discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md delete mode 100644 discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md delete mode 100644 discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md delete mode 100644 discussion/workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md delete mode 100644 discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md delete mode 100644 discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md delete mode 100644 discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md delete mode 100644 discussion/workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md delete mode 100644 discussion/workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md delete mode 100644 discussion/workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md delete mode 100644 discussion/workflow/plans/PLN-0025-final-ci-validation-and-polish.md delete mode 100644 discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md delete mode 100644 discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md delete mode 100644 discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md delete mode 100644 discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md diff --git a/discussion/.backups/index.ndjson.20260418-162736.bak b/discussion/.backups/index.ndjson.20260418-162736.bak new file mode 100644 index 00000000..68838273 --- /dev/null +++ b/discussion/.backups/index.ndjson.20260418-162736.bak @@ -0,0 +1,29 @@ +{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"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"}]} +{"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} +{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} +{"type":"discussion","id":"DSC-0002","status":"open","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0001","file":"workflow/agendas/AGD-0001-runtime-edge-test-plan.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"workflow/agendas/AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0004","status":"open","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0003","file":"workflow/agendas/AGD-0003-system-run-cart.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"workflow/agendas/AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[{"id":"AGD-0007","file":"workflow/agendas/AGD-0007-perf-runtime-telemetry-hot-path.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0005","file":"workflow/decisions/DEC-0005-perf-push-based-telemetry-model.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0005","file":"workflow/plans/PLN-0005-perf-push-based-telemetry-implementation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0026","file":"lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} +{"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0012","status":"open","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0011","file":"workflow/agendas/AGD-0011-perf-runtime-introspection-syscalls.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"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-15","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-15"}],"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-15","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-15","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-15","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-15","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-15","ref_decisions":["DEC-0014"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0028","status":"open","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[{"id":"AGD-0028","file":"workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18"}],"decisions":[{"id":"DEC-0016","file":"workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_agenda":"AGD-0028"}],"plans":[{"id":"PLN-0026","file":"workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0027","file":"workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0028","file":"workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0029","file":"workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]}],"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"}]} +{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"workflow/decisions/DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} +{"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} +{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"workflow/agendas/AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"workflow/decisions/DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"workflow/plans/PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} diff --git a/discussion/.backups/index.ndjson.20260418-162925.bak b/discussion/.backups/index.ndjson.20260418-162925.bak new file mode 100644 index 00000000..9ca7cece --- /dev/null +++ b/discussion/.backups/index.ndjson.20260418-162925.bak @@ -0,0 +1,29 @@ +{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"LSN":32,"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"}]} +{"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} +{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} +{"type":"discussion","id":"DSC-0002","status":"open","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0001","file":"workflow/agendas/AGD-0001-runtime-edge-test-plan.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"workflow/agendas/AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0004","status":"open","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0003","file":"workflow/agendas/AGD-0003-system-run-cart.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"workflow/agendas/AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[{"id":"AGD-0007","file":"workflow/agendas/AGD-0007-perf-runtime-telemetry-hot-path.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0005","file":"workflow/decisions/DEC-0005-perf-push-based-telemetry-model.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0005","file":"workflow/plans/PLN-0005-perf-push-based-telemetry-implementation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0026","file":"lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} +{"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0012","status":"open","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0011","file":"workflow/agendas/AGD-0011-perf-runtime-introspection-syscalls.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"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":"done","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-18","tags":["gfx","runtime","render","camera","scene"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"lessons/DSC-0026-render-all-scene-cache-and-camera-integration/LSN-0031-frame-composition-belongs-above-the-render-backend.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} +{"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0028","status":"open","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[{"id":"AGD-0028","file":"workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18"}],"decisions":[{"id":"DEC-0016","file":"workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_agenda":"AGD-0028"}],"plans":[{"id":"PLN-0026","file":"workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0027","file":"workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0028","file":"workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0029","file":"workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]}],"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"}]} +{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"workflow/decisions/DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} +{"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} +{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"workflow/agendas/AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"workflow/decisions/DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"workflow/plans/PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 68838273..6c1c7a0d 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"LSN":31,"CLSN":1}} +{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"LSN":34,"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,9 +18,9 @@ {"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-15","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-15"}],"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-15","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-15","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-15","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-15","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-15","ref_decisions":["DEC-0014"]}],"lessons":[]} -{"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]} -{"type":"discussion","id":"DSC-0028","status":"open","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[{"id":"AGD-0028","file":"workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18"}],"decisions":[{"id":"DEC-0016","file":"workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_agenda":"AGD-0028"}],"plans":[{"id":"PLN-0026","file":"workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0027","file":"workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0028","file":"workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0029","file":"workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0026","status":"done","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-18","tags":["gfx","runtime","render","camera","scene"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"lessons/DSC-0026-render-all-scene-cache-and-camera-integration/LSN-0031-frame-composition-belongs-above-the-render-backend.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} +{"type":"discussion","id":"DSC-0027","status":"done","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-18","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"lessons/DSC-0027-frame-composer-public-syscall-surface/LSN-0032-public-abi-must-follow-the-canonical-service-boundary.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} +{"type":"discussion","id":"DSC-0028","status":"done","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"lessons/DSC-0028-deferred-overlay-and-primitive-composition/LSN-0033-debug-primitives-should-be-a-final-overlay-not-part-of-game-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} {"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"}]} diff --git a/discussion/lessons/DSC-0026-render-all-scene-cache-and-camera-integration/LSN-0031-frame-composition-belongs-above-the-render-backend.md b/discussion/lessons/DSC-0026-render-all-scene-cache-and-camera-integration/LSN-0031-frame-composition-belongs-above-the-render-backend.md new file mode 100644 index 00000000..ace30888 --- /dev/null +++ b/discussion/lessons/DSC-0026-render-all-scene-cache-and-camera-integration/LSN-0031-frame-composition-belongs-above-the-render-backend.md @@ -0,0 +1,74 @@ +--- +id: LSN-0031 +ticket: render-all-scene-cache-and-camera-integration +title: Frame Composition Belongs Above the Render Backend +created: 2026-04-18 +tags: [gfx, runtime, render, camera, scene, sprites, frame-composer] +--- + +## Context + +`DSC-0025` split canonical scene ownership from viewport caching and resolver policy, but the runtime still treated `Gfx.render_all()` as the operational frame entrypoint. That left scene binding, camera state, cache refresh, and sprite submission spread across the wrong layer. + +`DSC-0026` completed the integration by making `FrameComposer` the owner of frame orchestration and reducing `Gfx` to a backend that consumes already prepared render state. + +## Key Decisions + +### Frame Orchestration Must Not Live in the Backend + +**What:** +`FrameComposer.render_frame()` became the canonical frame service, while `Gfx.render_all()` was retired from the runtime-facing flow. + +**Why:** +The backend should execute composition, not decide scene binding, camera policy, cache refresh, or sprite lifecycle. Keeping those responsibilities above `Gfx` preserves a cleaner ownership model and avoids re-entangling policy with raster code. + +**Trade-offs:** +This adds an explicit orchestration layer between runtime callsites and the renderer, but the resulting boundaries are easier to evolve and test. + +### Scene Binding, Camera, Cache, and Sprites Form One Operational Unit + +**What:** +`FrameComposer` owns: +- active scene binding by bank id and shared scene reference; +- camera coordinates in top-left world pixel space; +- `SceneViewportCache` and `SceneViewportResolver`; +- a frame-emission `SpriteController`. + +**Why:** +These concerns all define what a frame is. Splitting them across multiple owners would recreate stale-state bugs and make no-scene behavior ambiguous. + +**Trade-offs:** +The composer becomes a richer subsystem, but it carries policy in one place instead of leaking it into unrelated APIs. + +### The World Path Must Stay Tile-Size Agnostic + +**What:** +The integrated frame path derives cache sizing, resolver math, and world-copy behavior from per-layer scene metadata instead of a hard-coded `16x16` assumption. + +**Why:** +The scene contract already allows canonical `8x8`, `16x16`, and `32x32` tile sizes. The frame service has to consume that contract faithfully or it becomes a hidden compatibility break. + +**Trade-offs:** +The integration and tests need to exercise more than the legacy default path, but the renderer no longer bakes in a false invariant. + +## Patterns and Algorithms + +- Put frame policy in a dedicated orchestration layer and keep the renderer backend-oriented. +- Treat scene binding, camera state, cache lifetime, and sprite submission as one cohesive frame model. +- Refresh cache state inside the orchestrator before composition instead of letting the renderer discover refresh policy. +- Prefer frame-emission sprite submission with internal ordering over caller-owned sprite slots. +- Keep the no-scene path valid so world composition remains optional, not mandatory. + +## Pitfalls + +- Leaving `render_all()` alive as a canonical path creates a fragile dual-service model. +- Letting `Gfx` own cache refresh semantics collapses the boundary between policy and execution. +- Requiring a scene for every frame quietly breaks sprite-only or fade-only operation. +- Testing only `16x16` scenes hides regressions against valid `8x8` or `32x32` content. + +## Takeaways + +- Frame composition belongs in a subsystem that owns policy, not in the backend that draws pixels. +- Scene binding, camera, cache, resolver, and sprite submission should converge under one frame owner. +- No-scene rendering is part of the contract and should stay valid throughout integration work. +- Tile-size assumptions must be derived from canonical scene metadata, never from renderer habit. diff --git a/discussion/lessons/DSC-0027-frame-composer-public-syscall-surface/LSN-0032-public-abi-must-follow-the-canonical-service-boundary.md b/discussion/lessons/DSC-0027-frame-composer-public-syscall-surface/LSN-0032-public-abi-must-follow-the-canonical-service-boundary.md new file mode 100644 index 00000000..c0012645 --- /dev/null +++ b/discussion/lessons/DSC-0027-frame-composer-public-syscall-surface/LSN-0032-public-abi-must-follow-the-canonical-service-boundary.md @@ -0,0 +1,56 @@ +--- +id: LSN-0032 +ticket: frame-composer-public-syscall-surface +title: Public ABI Must Follow the Canonical Service Boundary +created: 2026-04-18 +tags: [gfx, runtime, syscall, abi, frame-composer, scene, camera, sprites] +--- + +## Context + +`DSC-0026` finished the internal migration to `FrameComposer` as the canonical frame-orchestration owner, but the public VM-facing ABI still exposed part of that behavior through legacy `gfx`-domain calls. That left the codebase with a mismatch between the real runtime ownership model and the surface visible to cartridges, tooling, and syscall declarations. + +`DSC-0027` closed that gap by introducing the `composer.*` public domain and removing the old public sprite path. + +## Key Decisions + +### Public Syscalls Must Expose the Real Owner + +**What:** +Scene binding, camera control, and sprite emission now live under `composer.*`, and the legacy public `gfx.set_sprite` path is gone. + +**Why:** +Once `FrameComposer` became the canonical orchestration service, keeping public orchestration under `gfx.*` would preserve the wrong mental model and encourage callers to treat the render backend as the owner of frame policy. + +**Trade-offs:** +This forces migration across ABI declarations, runtime dispatch, tests, and tooling, but it removes the long-term cost of a misleading public boundary. + +### Domain-Specific Status Types Preserve Architectural Meaning + +**What:** +Mutating public composer operations return `ComposerOpStatus` instead of reusing backend-oriented status naming. + +**Why:** +Operational outcomes for scene binding or sprite emission are not backend-domain results. Reusing `GfxOpStatus` would blur the boundary that the migration was trying to make explicit. + +**Trade-offs:** +This adds one more status family to maintain, but it keeps the public ABI semantically aligned with the actual service contract. + +## Patterns and Algorithms + +- Promote internal ownership changes into the public ABI as part of the same migration thread. +- Use syscall domains to encode service boundaries, not just namespace aesthetics. +- Remove obsolete public fallbacks completely when they preserve the wrong operational model. +- Keep runtime dispatch, bytecode declarations, and tooling aligned so the public path is exercised end to end. + +## Pitfalls + +- Leaving a legacy public syscall alive after the internal model changes creates a dual-contract system that is harder to remove later. +- Migrating runtime dispatch without migrating declarations and tooling can leave hidden ABI drift in tests and generators. +- Reusing backend-specific status names in the wrong domain quietly leaks old ownership assumptions into new APIs. + +## Takeaways + +- The public ABI should mirror the canonical service boundary, not historical implementation leftovers. +- Namespace changes are architectural when they change who is responsible for a behavior. +- Removing a legacy public entrypoint is often safer than preserving a compatibility shim that encodes the wrong model. diff --git a/discussion/lessons/DSC-0028-deferred-overlay-and-primitive-composition/LSN-0033-debug-primitives-should-be-a-final-overlay-not-part-of-game-composition.md b/discussion/lessons/DSC-0028-deferred-overlay-and-primitive-composition/LSN-0033-debug-primitives-should-be-a-final-overlay-not-part-of-game-composition.md new file mode 100644 index 00000000..76a16cc1 --- /dev/null +++ b/discussion/lessons/DSC-0028-deferred-overlay-and-primitive-composition/LSN-0033-debug-primitives-should-be-a-final-overlay-not-part-of-game-composition.md @@ -0,0 +1,56 @@ +--- +id: LSN-0033 +ticket: deferred-overlay-and-primitive-composition +title: Debug Primitives Should Be a Final Overlay, Not Part of Game Composition +created: 2026-04-18 +tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud, debug] +--- + +## Context + +After `FrameComposer.render_frame()` became the canonical game-frame entrypoint, immediate `gfx.*` primitive writes were no longer stable. Scene-backed composition could rebuild the framebuffer after `draw_text(...)` or other debug primitives had already written to it. + +`DSC-0028` resolved that conflict by moving `gfx.*` primitives into a deferred overlay/debug stage outside `FrameComposer`, drained only after canonical game composition and fades are complete. + +## Key Decisions + +### Debug Overlay Must Stay Outside the Canonical Game Pipeline + +**What:** +`FrameComposer` keeps ownership of canonical game composition, while debug/text/primitive commands are captured separately and drained later as a final overlay. + +**Why:** +Game composition and debug overlay have different purposes. The first must remain canonical and deterministic; the second must remain opportunistic, screen-space, and independent from scene or sprite semantics. + +**Trade-offs:** +The renderer needs a second deferred path, but the game pipeline no longer depends on transient debug state. + +### Final Visual Ordering Matters More Than Immediate Writes + +**What:** +Overlay/debug commands are drained after scene composition, sprite composition, and fades, with parity between scene-bound and no-scene frame paths. + +**Why:** +The stable user-visible contract is that debug primitives appear on top. Immediate writes were only an implementation detail, and they stopped preserving that contract once frame composition became deferred and canonical. + +**Trade-offs:** +This changes primitive semantics from "write now" to "show at frame end," but it produces the behavior users actually rely on. + +## Patterns and Algorithms + +- Separate canonical composition state from debug-overlay state even when both reuse the same raster backend. +- Capture primitives as commands first, then drain them at the final stage where visual priority is unambiguous. +- Preserve the same overlay semantics whether a scene is bound or not. +- Keep implementation reuse internal while maintaining a clear semantic boundary in the public model. + +## Pitfalls + +- Treating debug primitives as part of HUD or scene composition will eventually couple tooling/debug behavior to gameplay pipeline rules. +- Draining overlay before fades or before final frame composition breaks the visible "always on top" contract. +- Reusing `FrameComposer` storage for overlay state collapses the ownership split that prevents these bugs. + +## Takeaways + +- Immediate framebuffer writes are not a reliable contract once final composition is orchestrated elsewhere. +- Debug primitives work best as a dedicated final overlay layer. +- Ownership separation is what keeps debug behavior stable while the canonical render pipeline evolves. diff --git a/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md b/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md deleted file mode 100644 index 756daaea..00000000 --- a/discussion/workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md +++ /dev/null @@ -1,405 +0,0 @@ ---- -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-15 -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 -- 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` 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`; - - 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` 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. diff --git a/discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md b/discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md deleted file mode 100644 index 560720cd..00000000 --- a/discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -id: AGD-0027 -ticket: frame-composer-public-syscall-surface -title: Agenda - FrameComposer Public Syscall Surface -status: accepted -created: 2026-04-17 -updated: 2026-04-17 -tags: [gfx, runtime, syscall, abi, frame-composer, scene, camera, sprites] ---- - -## Contexto - -`DEC-0014` e os planos `PLN-0017` a `PLN-0021` fecharam a migração interna do pipeline de frame para `FrameComposer`: - -- `FrameComposer` virou o orquestrador canônico do frame; -- `Hardware` passou a agregá-lo ao lado de `Gfx`; -- scene, camera, cache, resolver e sprite emission migraram para ownership interno dele; -- o frame loop do runtime passou a renderizar via `FrameComposer.render_frame()`. - -Isso resolveu a base operacional interna, mas não fechou a superfície pública equivalente para a VM. A ABI pública ainda expõe apenas o contrato legado de `gfx.set_sprite(...)`, enquanto `bind_scene(...)` e `set_camera(...)` existem apenas como APIs internas do driver. - -Na prática, hoje temos uma assimetria: - -- a base canônica do frame está em `FrameComposer`; -- mas a ABI pública ainda não trata `FrameComposer` como serviço canônico para scene, camera e sprites. - -Essa lacuna impede a migração do restante da stack e também impede um stress cartridge que atravesse de verdade o pipeline novo por syscall pública. - -## Problema - -Precisamos definir a nova superfície pública de syscall para o pipeline canônico de `FrameComposer` sem reabrir a decisão já aceita sobre ownership interno do frame. - -O problema concreto não é “adicionar 2 ou 3 syscalls”. Precisamos decidir: - -- quais operações de `FrameComposer` viram ABI pública agora; -- se `gfx.set_sprite(...)` continua como shim legado ou perde status canônico; -- qual é o contrato mínimo de scene/camera que a VM pode observar/controlar; -- como nomear e versionar essa superfície pública sem criar um segundo modelo canônico concorrente; -- qual é a estratégia de transição para cartridge, runtime tests e stress tests; -- como propagar essa mudança para a spec canônica e, se necessário, para contratos de ABI e `ISA_CORE`. - -## Pontos Criticos - -- `DEC-0014` já fechou `FrameComposer` como base canônica interna; esta agenda não deve reabrir isso. -- A ABI pública atual ainda expõe `gfx.set_sprite(...)` com semântica herdada de índice/slot, mesmo que a implementação interna já use frame emission. -- `bind_scene(scene_bank_id)` e `set_camera(x, y)` já existem no driver, mas ainda não existem como syscalls públicas. -- Se a nova ABI expuser demais logo de início, vamos congelar cedo demais detalhes que ainda não provaram valor operacional. -- Se a nova ABI expuser de menos, manteremos um modelo híbrido por tempo demais: - - canônico internamente via `FrameComposer`; - - legado externamente via `Gfx`/`set_sprite`. -- Precisamos decidir se o namespace público continua em `gfx.*` por estabilidade do domínio, ou se devemos introduzir algo como `frame.*`. -- A transição precisa preservar compatibilidade suficiente para não quebrar cartridges e testes existentes antes da migração do restante. -- O contrato de sprite precisa deixar claro se o chamador ainda informa índice, se informa `layer`, e se `active` continua existindo na superfície pública. -- A mudança não pode ficar só em código/runtime; a spec canônica precisa ser atualizada para refletir o novo serviço público. -- Se o contrato público afetar superfícies documentadas de ABI ou o material de `ISA_CORE`, essa propagação precisa ser tratada como parte da mesma thread, não como follow-up solto. - -## Opcoes - -### Opcao 1 - Expor um núcleo mínimo canônico em `gfx.*` - -**Como seria:** -Adicionar apenas a superfície mínima para a VM controlar o pipeline novo: - -- `gfx.bind_scene(bank_id)` -- `gfx.unbind_scene()` -- `gfx.set_camera(x, y)` -- `gfx.emit_sprite(...)` - -`gfx.set_sprite(...)` permaneceria por um período como shim legado de compatibilidade. - -**Vantagens:** -- fecha rapidamente a lacuna operacional; -- habilita stress real do pipeline novo; -- reduz o tempo de convivência entre modelo canônico e legado; -- mantém o domínio público em `gfx`, evitando churn de namespace. - -**Desvantagens:** -- introduz ABI nova que precisará de migração coordenada; -- exige definir `emit_sprite(...)` com cuidado para não herdar sem querer o modelo de slot. - -### Opcao 2 - Expor scene/camera agora e adiar o contrato novo de sprite - -**Como seria:** -Publicar apenas: - -- `gfx.bind_scene(bank_id)` -- `gfx.unbind_scene()` -- `gfx.set_camera(x, y)` - -Sprites continuariam publicamente via `gfx.set_sprite(...)` até uma segunda fase. - -**Vantagens:** -- menor mudança imediata de ABI; -- desbloqueia o stress do world path e da câmera; -- reduz o volume inicial da migração pública. - -**Desvantagens:** -- mantém dois modelos públicos de sprite por mais tempo; -- prolonga a semântica de compatibilidade do syscall legado; -- adia exatamente uma das partes centrais da migração para `FrameComposer`. - -### Opcao 3 - Criar um novo namespace público separado, como `composer.*` - -**Como seria:** -O pipeline novo ganha syscalls em um domínio separado, por exemplo: - -- `composer.bind_scene` -- `composer.unbind_scene` -- `composer.set_camera` -- `composer.emit_sprite` - -`gfx.*` ficaria como superfície legacy/low-level. - -**Vantagens:** -- deixa explícita a mudança de serviço canônico; -- evita sobrecarregar semanticamente `gfx`. - -**Desvantagens:** -- adiciona churn conceitual e de nomenclatura; -- fragmenta demais a superfície pública neste momento; -- cria um custo de transição maior sem benefício operacional evidente. - -## Sugestao / Recomendacao - -Seguir com a **Opcao 3**. - -Direção recomendada: - -- a superfície pública canônica deve migrar para o domínio `composer.*`; -- `FrameComposer` vira a base canônica também na ABI pública, com namespace próprio em vez de continuar semanticamente preso a `gfx.*`; -- o núcleo mínimo público deve ser: - - `composer.bind_scene(bank_id) -> status` - - `composer.unbind_scene()` - - `composer.set_camera(x, y)` - - `composer.emit_sprite(...) -> status` -- `gfx.set_sprite(...)` deve morrer e ser removido completamente do contrato público. - -Para sprites, a recomendação provisória é: - -- a nova ABI pública não deve exigir índice explícito; -- `composer.emit_sprite(...)` deve receber o payload completo necessário para o frame: - - `glyph_id` - - `palette_id` - - `x` - - `y` - - `layer` - - `bank_id` - - `flip_x` - - `flip_y` - - `priority` -- a ABI pode futuramente agrupar esse payload se isso melhorar ergonomia, mas o contrato mínimo deve nascer completo; -- `active` não deve continuar no contrato canônico novo; -- overflow continua sendo ignorado com status/telemetria adequada, sem trapar o runtime. - -Para scene/camera, a recomendação provisória é: - -- manter o contrato mínimo já aceito internamente; -- `bind_scene` por bank id; -- `unbind_scene` explícito; -- `set_camera(x, y)` em pixel space com top-left viewport. -- `bind_scene(...)`, `unbind_scene(...)` e `emit_sprite(...)` devem usar `ComposerOpStatus` como retorno operacional canônico. - -## Perguntas em Aberto - -- Resolvido: - - o nome público canônico de sprite será `composer.emit_sprite(...)`; - - o syscall novo de sprite nasce completo com `glyph_id`, `palette_id`, `x`, `y`, `layer`, `bank_id`, `flip_x`, `flip_y`, `priority`; - - `gfx.set_sprite(...)` deve morrer e ser removido completamente; - - não haverá leitura de estado nesta primeira fase; - - `bind_scene(...)`, `unbind_scene(...)` e `emit_sprite(...)` usarão `ComposerOpStatus`; -- A ABI nova precisa expor refresh explícito, ou isso deve continuar totalmente interno ao `FrameComposer`? -- Resolvido: - - a ABI nova não deve expor refresh explícito; - - o domínio público canônico será `composer.*`, não `gfx.*`. - -## Criterio para Encerrar - -Esta agenda pode ser encerrada quando houver acordo explícito sobre: - -- a lista mínima de syscalls públicas canônicas do `FrameComposer`; -- o nome canônico da operação pública de sprite; -- a remoção completa de `gfx.set_sprite(...)` do contrato público; -- o formato de retorno/status das novas operações; -- a estratégia de transição necessária para decisão, plano e migração do restante da stack. - -## Resolucao em Andamento - -Direção atualmente acordada nesta agenda: - -- o namespace público canônico será `composer.*`; -- o núcleo mínimo inicial será: - - `composer.bind_scene(bank_id) -> ComposerOpStatus` - - `composer.unbind_scene() -> ComposerOpStatus` - - `composer.set_camera(x, y)` - - `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> ComposerOpStatus` -- não haverá introspecção pública nesta primeira fase; -- refresh/cache policy continua interno ao `FrameComposer`; -- `gfx.set_sprite(...)` não terá caminho de compatibilidade e deve ser removido. - -## Resolucao - -Esta agenda fica aceita com os seguintes pontos fechados: - -- o namespace público canônico do serviço será `composer.*`; -- a superfície mínima inicial será: - - `composer.bind_scene(bank_id) -> ComposerOpStatus` - - `composer.unbind_scene() -> ComposerOpStatus` - - `composer.set_camera(x, y)` - - `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> ComposerOpStatus` -- não haverá introspecção pública nesta primeira fase; -- não haverá refresh/cache policy público; -- `gfx.set_sprite(...)` deve ser removido completamente, sem shim de compatibilidade; -- a transição deve introduzir `composer.*` e remover `gfx.set_sprite(...)` na mesma thread de migração, com atualização coordenada de bytecode, cartridges, tests e runtime; -- a mesma thread deve atualizar a spec canônica do assunto e propagar a mudança para contratos de ABI e `ISA_CORE` quando essas superfícies forem impactadas pelo novo serviço público. diff --git a/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md b/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md deleted file mode 100644 index 677f31b7..00000000 --- a/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -id: AGD-0028 -ticket: deferred-overlay-and-primitive-composition -title: Deferred Overlay and Primitive Composition over FrameComposer -status: accepted -created: 2026-04-18 -updated: 2026-04-18 -resolved: 2026-04-18 -decision: DEC-0016 -tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud] ---- - -## Contexto - -`FrameComposer.render_frame()` hoje recompõe o `back` no fim da logical frame. Quando há scene bound, o caminho `render_scene_from_cache(...)` limpa o buffer e desenha scene + sprites, o que apaga qualquer primitive ou `draw_text(...)` emitido antes via `gfx`. - -Isso expôs um conflito de modelo: - -- `composer.*` já é o caminho canônico de orquestração de frame; -- `gfx.draw_text(...)` e demais primitives ainda escrevem diretamente no `back`; -- o runtime só chama `render_frame()` no final do frame, então a escrita imediata em `back` deixou de ser semanticamente estável. -- As primitives de `gfx` não são o mecanismo desejado para composição de jogos com scene/tile/sprite; elas existem principalmente como debug, instrumentação visual e artefatos rápidos. - -Conteúdo relevante migrado de [AGD-0010](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md): - -- a arquitetura aceita continua sendo de framebuffer destrutivo em memória, não scene graph ou renderer tipo GPU; -- otimizações em primitives devem preservar a semântica observável, mesmo quando ganharem fast paths internos; -- existe preocupação explícita com custo por classe de primitive e com orçamento de memória no alvo handheld; -- caminhos de spans/linhas/clears são desejáveis como aceleração interna, mas sem reabrir o modelo operacional do pipeline do jogo. - -## Problema - -Precisamos decidir qual é o modelo canônico para primitives e texto no pipeline pós-`FrameComposer`. - -Sem isso: - -- texto e primitives continuam com comportamento dependente da ordem interna do renderer; -- o stress test e qualquer cartridge que combine `composer.*` com `gfx.*` terão resultado inconsistente; -- fica indefinido se primitives pertencem ao mundo, ao HUD, ou a um overlay final. - -## Pontos Criticos - -- `draw_text(...)` e primitives screen-space não podem depender de escrita imediata em `back`. -- Para esta thread, primitives de `gfx` devem permanecer agnósticas ao pipeline canônico de render do jogo e não devem ser mescladas semanticamente com tiles/sprites. -- A ordem de composição precisa ser explícita e estável: `scene -> sprites -> HUD -> primitives/debug overlay`, ou outra ordem formal equivalente. -- Precisamos decidir se o contrato público de `gfx.*` muda semanticamente sem mudar ABI, ou se parte dessa superfície migra para `composer.*`. -- A solução deve preservar o caminho sem scene bound. -- A implementação deve evitar contaminar a infraestrutura de `gfx` responsável por scene, sprites e HUD com estado misto de overlay/debug; se necessário, o overlay deve ter fila/fase própria. -- melhorias internas de primitive path devem continuar permitidas, desde que não mudem a semântica de overlay final e não exijam buffers extras incompatíveis com o orçamento de memória aceito. - -## Opcoes - -### Opcao 1 - Manter escrita direta em `back` - -- **Abordagem:** manter `gfx.draw_text(...)` e primitives rasterizando imediatamente. -- **Pro:** zero mudança estrutural agora. -- **Contra:** o modelo continua quebrado sempre que `render_frame()` recompõe o buffer depois. -- **Tradeoff:** só funciona de forma confiável fora do caminho canônico do `FrameComposer`. - -### Opcao 2 - Fila única de draw commands pós-scene/pós-sprite - -- **Abordagem:** transformar texto e primitives em comandos diferidos, drenados depois de `scene + sprites`. -- **Pro:** resolve o problema imediato de overlay/HUD e estabiliza o stress test. -- **Contra:** mistura HUD e primitives/debug sob o mesmo conceito, reduzindo clareza contratual mesmo quando a ordem prática for a mesma. -- **Tradeoff:** simples para V1, mas semanticamente mais fraco do que separar overlay de jogo e overlay de debug. - -### Opcao 3 - Separar HUD diferido de primitives/debug overlay final - -- **Abordagem:** tratar `gfx.draw_text(...)` e demais primitives de `gfx` como overlay/debug final, separado da composição canônica de jogo (`scene + sprites + HUD`). -- **Pro:** casa com a intenção declarada para `gfx.*`: debug, artefato rápido e instrumentação visual acima do frame do jogo. -- **Contra:** exige modelar explicitamente uma fase extra no pipeline. -- **Tradeoff:** aumenta a clareza contratual e evita mesclar primitives com o domínio de jogo. - -### Opcao 4 - Manter HUD e primitives no mesmo estágio final, mas com categorias separadas - -- **Abordagem:** drenar HUD e primitives ambos no fim do frame, porém com filas/categorias distintas e ordem formal `HUD -> primitives`. -- **Pro:** preserva implementação próxima entre caminhos similares, mantendo contrato separado. -- **Contra:** é mais custoso que a opção 3 sem entregar muito valor adicional imediato. -- **Tradeoff:** bom se já houver expectativa de HUD canônico separado no curtíssimo prazo. - -## Sugestao / Recomendacao - -Seguir com a **Opcao 3**. - -Minha recomendação é: - -- retirar a escrita direta em `back` como contrato operacional para `gfx.draw_text(...)` e demais primitives de `gfx`; -- introduzir uma fila diferida canônica de primitives/debug overlay drenada no fim do frame; -- tratar `gfx.*` primitive/text como superfície agnóstica ao pipeline de jogo e explicitamente acima da composição canônica; -- não misturar semanticamente primitives com scene/tile/sprite/HUD. -- evitar compartilhar indevidamente o mesmo mecanismo operacional de composição entre overlay/debug e os caminhos de scene/sprite/HUD, mesmo quando o backend de rasterização reutilizado for o mesmo. - -Ordem recomendada para o frame canônico: - -1. limpar/compor scene; -2. compor sprites; -3. compor HUD canônico, se existir; -4. aplicar `scene_fade`; -5. aplicar `hud_fade`; -6. drenar primitives/debug overlay de `gfx.*`. - -## Perguntas em Aberto - -- `draw_text(...)` e as demais primitives de `gfx` entram todas na mesma família de overlay final já na V1, ou começamos só com `draw_text(...)`? -- `render_no_scene_frame()` deve usar a mesma fila diferida para manter semântica idêntica com e sem scene? -- HUD canônico precisa existir explicitamente nesta mesma thread, ou pode continuar implícito/externo enquanto as primitives já migram para overlay final? -- quais fast paths internos de primitives continuam desejáveis nessa nova fase, por exemplo spans horizontais/verticais, fills e clears, sem misturar isso com a composição do jogo? -- o overlay/debug final precisa de dirtying próprio por classe de primitive ou isso pode ficar fora da primeira migração? - -## Criterio para Encerrar - -Esta agenda pode ser encerrada quando tivermos uma resposta explícita para: - -- o destino semântico de `draw_text(...)`; -- se haverá uma fila própria para primitives/debug overlay e qual a relação dela com HUD; -- a ordem canônica de composição do frame; -- o escopo exato da primeira migração implementável sem reabrir o restante do pipeline. - -## Resolucao Parcial - -Direção já aceita nesta agenda: - -- primitives e `draw_text(...)` de `gfx.*` devem ser tratadas como overlay/debug final; -- esse overlay deve ser drenado **depois** de `hud_fade`; -- scene, sprites e HUD canônico não devem ser semanticamente misturados com o overlay/debug; -- a implementação deve preservar separação operacional suficiente para que o `gfx` usado pelo pipeline do jogo não passe a depender do estado transitório de primitives/debug; -- otimizações de primitive path discutidas na `AGD-0010` continuam válidas, mas passam a operar dentro do domínio de overlay/debug final, não como parte da composição canônica de scene/sprite/HUD. - -## Resolucao - -Esta agenda fica aceita com os seguintes pontos fechados: - -- `gfx.draw_text(...)` e as demais primitives públicas de `gfx.*` pertencem à mesma família V1 de overlay/debug final; -- esse overlay/debug fica **fora** do `FrameComposer`; -- `FrameComposer` continua restrito à composição canônica do jogo (`scene`, `sprites` e HUD canônico quando existir); -- o overlay/debug deve ser drenado depois de `hud_fade`; -- o caminho sem scene bound deve observar a mesma semântica final de overlay/debug; -- HUD canônico explícito não faz parte desta thread e pode permanecer implícito/externo por enquanto; -- fast paths internos de primitives continuam permitidos, desde que preservem a semântica observável do overlay/debug final; -- dirtying granular ou otimizações finas por classe de primitive não fazem parte da primeira migração normativa desta thread. diff --git a/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md b/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md deleted file mode 100644 index db95ec61..00000000 --- a/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -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. - -It also preserves the scene-model contract already accepted below the frame layer: - -- tile size is a property of each scene layer; -- the frame orchestrator must consume that contract, not redefine it; -- `FrameComposer` must therefore remain tile-size agnostic rather than hard-coding `16x16` assumptions. - -## 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` 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. - -### 5A. Tile Size Contract - -- `FrameComposer` SHALL remain tile-size agnostic. -- `FrameComposer` MUST accept scene layers whose canonical `tile_size` is `8x8`, `16x16`, or `32x32`. -- `FrameComposer` MUST NOT impose `16x16` as a bind-time, cache-time, resolver-time, or render-time precondition. -- Cache sizing, resolver math, and world-copy preparation SHALL derive from the `tile_size` declared by each bound scene layer. -- Compatibility checks, when needed, MUST be derived from canonical scene-layer and glyph-bank metadata rather than from a hard-coded compositor default. -- Any implementation path that only works for `16x16` tiles is NON-COMPLIANT with this decision. - -### 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`. -- `FrameComposer`, cache, and resolver integration must preserve per-layer `tile_size` semantics, including `8x8`. - -### 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. -- Scene-driven tile-size metadata must propagate unchanged into `FrameComposer` orchestration and backend copy preparation. - -## 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. -- Plan steps and tests that cover world composition MUST explicitly include `8x8` tile-size coverage. - -## Revision Log - -- 2026-04-14: Initial accepted decision from `AGD-0026`. -- 2026-04-15: Revision accepted to make `FrameComposer` explicitly tile-size agnostic and to require `8x8` support alongside `16x16` and `32x32`. diff --git a/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md b/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md deleted file mode 100644 index f0fb8fba..00000000 --- a/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -id: DEC-0015 -ticket: frame-composer-public-syscall-surface -title: FrameComposer Public Syscall Surface -status: accepted -created: 2026-04-17 -accepted: 2026-04-17 -agenda: AGD-0027 -plans: [PLN-0022, PLN-0023, PLN-0024, PLN-0025] -tags: [gfx, runtime, syscall, abi, frame-composer, scene, camera, sprites] ---- - -## Status - -Accepted. - -## Contexto - -`DEC-0014` locked `FrameComposer` as the canonical internal frame orchestration service and `PLN-0017` through `PLN-0021` completed that internal migration path. `Hardware` now owns `FrameComposer`, the runtime renders through `FrameComposer.render_frame()`, and scene/camera/cache/resolver/sprite ownership no longer belongs canonically to `Gfx`. - -That migration did not define the equivalent public syscall contract for VM code. The public ABI still exposed legacy `gfx`-domain sprite control while the canonical scene/camera operations existed only as internal driver APIs. - -This decision closes that public ABI gap without reopening the already accepted internal ownership model. - -## Decisao - -The canonical public syscall surface for frame orchestration SHALL move to the `composer.*` namespace. - -Normatively: - -- The canonical public service domain for `FrameComposer` operations SHALL be `composer`. -- The initial canonical syscall set SHALL be: - - `composer.bind_scene(bank_id) -> ComposerOpStatus` - - `composer.unbind_scene() -> ComposerOpStatus` - - `composer.set_camera(x, y)` - - `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> ComposerOpStatus` -- `composer.emit_sprite(...)` SHALL be the canonical public sprite submission path. -- `composer.emit_sprite(...)` MUST NOT require a caller-provided sprite index. -- `composer.emit_sprite(...)` MUST carry `layer` and `priority`. -- `composer.emit_sprite(...)` MUST NOT expose `active` as part of the canonical contract. -- `composer.bind_scene(...)`, `composer.unbind_scene()`, and `composer.emit_sprite(...)` SHALL return `ComposerOpStatus`. -- `composer.set_camera(x, y)` SHALL keep the minimal V1 camera contract already accepted by `DEC-0014`: - - `x` and `y` are `i32` pixel coordinates; - - they represent the top-left viewport origin in world space. -- The public ABI MUST NOT expose cache refresh policy or explicit refresh controls. -- The public ABI MUST NOT expose scene/camera introspection in this first phase. -- `gfx.set_sprite(...)` MUST be removed completely from the public contract. -- No compatibility shim for `gfx.set_sprite(...)` SHALL remain as part of the canonical migration target. -- Introduction of `composer.*` and removal of `gfx.set_sprite(...)` SHALL be executed in the same migration thread. - -## Rationale - -The public ABI must reflect the accepted ownership model rather than preserve a misleading legacy shape. - -Keeping the canonical public surface under `gfx.*` would continue to tie orchestration semantics to the wrong service boundary. The new namespace makes the ownership change explicit: - -- `Gfx` is the visual backend; -- `FrameComposer` is the frame orchestration service. - -Removing `gfx.set_sprite(...)` completely avoids prolonging a dual public sprite model. A compatibility shim would preserve legacy slot/index semantics in the public contract after those semantics had already ceased to be canonical internally. - -Returning `ComposerOpStatus` for operational mutating calls preserves status-first behavior while keeping the public contract aligned with the new service boundary. Reusing `GfxOpStatus` would leak backend-domain semantics into orchestration-domain syscalls after that separation had already been made explicit. - -Deferring introspection and explicit refresh controls keeps the first public ABI focused on control, not diagnostics or internal policy leakage. - -## Invariantes / Contrato - -### 1. Namespace - -- Public frame-orchestration syscalls MUST live under `composer.*`. -- `composer.*` SHALL be treated as the canonical public orchestration surface. -- `gfx.*` SHALL NOT remain the canonical public orchestration namespace for scene/camera/sprite submission. - -### 2. Scene Control - -- `composer.bind_scene(bank_id)` MUST bind by scene bank id. -- Binding semantics MUST remain aligned with `DEC-0014`: - - scene resolution through the scene bank pool; - - explicit bind/unbind lifecycle; - - no implicit per-frame rebinding. -- `composer.unbind_scene()` MUST leave no-scene rendering valid. -- `ComposerOpStatus` SHALL be the canonical operational status family for composer-domain mutating syscalls. - -### 3. Camera - -- `composer.set_camera(x, y)` MUST remain the minimal V1 camera API. -- Camera follow, smoothing, shake, transitions, and readback are OUT OF SCOPE for this decision. - -### 4. Sprite Submission - -- `composer.emit_sprite(...)` MUST be frame-emission based. -- The caller MUST NOT provide sprite slot/index information. -- The public payload MUST include: - - `glyph_id` - - `palette_id` - - `x` - - `y` - - `layer` - - `bank_id` - - `flip_x` - - `flip_y` - - `priority` -- The canonical public sprite contract MUST NOT include `active`. -- Overflow behavior SHALL remain aligned with `DEC-0014`: - - excess sprites are ignored; - - overflow is not a hard VM fault in V1. - -### 5. Non-Goals for V1 Public ABI - -- No public refresh/invalidate syscalls. -- No public cache inspection syscalls. -- No public `scene_status()` syscall. -- No public `get_camera()` syscall. - -### 6. Migration Contract - -- Migration MUST update: - - syscall registry and ABI resolution; - - runtime dispatch; - - bytecode/cartridge declarations; - - tests; - - stress cartridges and related tooling where applicable. -- Migration MUST NOT leave `gfx.set_sprite(...)` as a supported public fallback after the new contract lands. - -## Impactos - -### HAL - -- The syscall enum, registry, metadata, and resolver will need a new `composer` domain surface. -- `gfx.set_sprite(...)` must be removed from the public ABI contract. -- A new `ComposerOpStatus` contract will need to be introduced for composer-domain operational returns. - -### Runtime / VM - -- Runtime dispatch must route public scene/camera/sprite orchestration through `FrameComposer`. -- Existing bytecode declarations and cartridges that rely on `gfx.set_sprite(...)` will need coordinated migration. - -### Spec / ABI / ISA_CORE - -- The canonical spec for the public VM-facing graphics/composition surface must be updated to reflect `composer.*`. -- ABI-facing documentation and contracts must be updated wherever syscall domain, names, arguments, or return semantics are specified. -- `ISA_CORE` must be updated if and where it normatively references the public syscall surface affected by this decision. - -### Drivers / Hardware - -- `FrameComposer` already has the required internal base; execution work will focus on public ABI exposure rather than internal ownership redesign. - -### Tooling / Stress - -- Stress cartridges and bytecode generators can only exercise the canonical frame path publicly after `composer.*` exists. - -## Referencias - -- [AGD-0027-frame-composer-public-syscall-surface.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md) -- [DEC-0014-frame-composer-render-integration.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md) - -## Propagacao Necessaria - -- A new implementation plan MUST be created from this decision before code changes. -- The plan MUST cover ABI introduction, legacy syscall removal, cartridge/test migration, regression coverage, and canonical spec propagation. -- The plan MUST explicitly assess and update ABI and `ISA_CORE` artifacts where this decision changes documented public behavior. -- Stress tooling SHOULD be updated as part of the migration thread so the public ABI can exercise the canonical frame path end-to-end. - -## Revision Log - -- 2026-04-17: Initial accepted decision from `AGD-0027`. diff --git a/discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md b/discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md deleted file mode 100644 index be739372..00000000 --- a/discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -id: DEC-0016 -ticket: deferred-overlay-and-primitive-composition -title: Deferred GFX Overlay Outside FrameComposer -status: accepted -created: 2026-04-18 -accepted: 2026-04-18 -agenda: AGD-0028 -plans: [PLN-0026, PLN-0027, PLN-0028, PLN-0029] -tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud] ---- - -## Status - -Accepted. - -## Contexto - -`DEC-0014` and `DEC-0015` established `FrameComposer` as the canonical orchestration path for game-frame composition and exposed that orchestration publicly through `composer.*`. - -That migration left `gfx.draw_text(...)` and other `gfx` primitives with their historical immediate-write behavior against the working framebuffer. Once the runtime moved to end-of-frame composition through `FrameComposer.render_frame()`, those immediate writes became unstable: scene-backed frame composition can rebuild the backbuffer after primitive calls have already touched it. - -The resulting conflict is not about whether primitives should remain available. It is about their semantic place in the pipeline. The accepted direction of this thread is that `gfx` primitives are not part of the canonical game composition model. They are primarily for debug, quick visual instrumentation, and rapid artifacts, and they must remain agnostic to scene/tile/sprite/HUD composition. - -Relevant performance context migrated from `AGD-0010` also remains in force: - -- the renderer continues to be a destructive software framebuffer model, not a retained scene graph or GPU-style renderer; -- internal primitive fast paths remain desirable; -- memory growth must remain constrained for the handheld target; -- optimization of primitive execution must not alter observable semantics. - -## Decisao - -`gfx.*` primitives and text SHALL move to a deferred final overlay model that lives outside `FrameComposer`. - -Normatively: - -- `FrameComposer` SHALL remain responsible only for canonical game-frame composition: - - scene composition; - - sprite composition; - - canonical HUD composition when such a HUD stage exists. -- `FrameComposer` MUST NOT become the owner of debug/primitive overlay state. -- Public `gfx.*` primitives, including `gfx.draw_text(...)`, SHALL belong to a V1 `gfx` overlay/debug family. -- That overlay/debug family SHALL be deferred rather than written immediately as the stable operational contract. -- The deferred overlay/debug stage SHALL be drained after `hud_fade`. -- The deferred overlay/debug stage SHALL be above scene, sprites, and canonical HUD in final visual order. -- The no-scene path MUST preserve the same final overlay/debug semantics. -- `gfx.*` primitives MUST remain semantically separate from scene/tile/sprite/HUD composition. -- The implementation MUST preserve operational separation sufficient to prevent the canonical game pipeline from depending on transient primitive/debug state. - -## Rationale - -This decision keeps the architectural boundary clean. - -`FrameComposer` exists to own the canonical game frame. Debug primitives do not belong to that contract. Pulling them into `FrameComposer` would make the orchestration service responsible for a second semantic domain with different goals: - -- game composition must be deterministic and canonical; -- primitive/text overlay must be opportunistic, screen-space, and pipeline-agnostic. - -Keeping overlay/debug outside `FrameComposer` also aligns with the stated product intent: these primitives are useful helpers, but they are not meant to become a second composition language for games. - -Draining them after `hud_fade` preserves the user-visible requirement that debug/overlay content stay truly on top and legible. This is more faithful to the accepted intent than treating primitives as part of HUD or world composition. - -Finally, separating semantic ownership still leaves room for implementation reuse. Raster backends, span paths, and buffer-writing helpers may still be shared internally, provided the public operational model remains separate. - -## Invariantes / Contrato - -### 1. Ownership Boundary - -- `FrameComposer` MUST own only canonical game-frame composition. -- Primitive/debug overlay state MUST live outside `FrameComposer`. -- The canonical game pipeline MUST NOT depend on primitive/debug overlay state for correctness. - -### 2. Overlay Semantics - -- `gfx.draw_text(...)` and sibling `gfx` primitives SHALL be treated as deferred final overlay/debug operations. -- Immediate direct writes to `back` MUST NOT remain the stable operational contract for these primitives. -- Final overlay/debug output MUST appear after: - - scene composition; - - sprite composition; - - canonical HUD composition, if present; - - `scene_fade`; - - `hud_fade`. - -### 3. Separation from Game Composition - -- Primitive/debug overlay MUST NOT be reinterpreted as scene content. -- Primitive/debug overlay MUST NOT be reinterpreted as sprite content. -- Primitive/debug overlay MUST NOT be the vehicle for canonical HUD composition. -- The public `gfx.*` primitive surface SHALL remain pipeline-agnostic relative to `composer.*`. - -### 4. Consistency Across Frame Paths - -- The scene-bound path and no-scene path MUST expose the same final overlay/debug behavior. -- Users MUST NOT need to know whether a scene is bound for `gfx.*` primitives to appear as final overlay/debug content. - -### 5. Internal Optimization Contract - -- Internal fast paths for lines, spans, fills, clears, or similar primitive operations MAY be introduced. -- Such fast paths MUST preserve the observable deferred overlay/debug semantics. -- This decision DOES NOT require fine-grained dirtying or per-primitive-class invalidation in the first migration. - -## Impactos - -### Runtime / Drivers - -- The runtime frame-end sequence must gain a distinct overlay/debug drain stage outside `FrameComposer`. -- `gfx.draw_text(...)` and peer primitives can no longer rely on stable immediate framebuffer writes once this migration lands. - -### GFX Backend - -- `Gfx` will need an explicit deferred overlay/debug command path or equivalent subsystem boundary. -- Shared raster helpers remain allowed, but the overlay/debug phase must stay semantically distinct from scene/sprite/HUD composition. - -### FrameComposer - -- `FrameComposer` must remain free of primitive/debug overlay ownership. -- Any future HUD integration must not collapse that boundary. - -### Spec / Docs - -- The canonical graphics/runtime spec must describe `gfx.*` primitives as deferred final overlay/debug operations rather than stable immediate backbuffer writes. -- Documentation that describes frame ordering must show overlay/debug after `hud_fade`. - -### Performance Follow-up - -- `AGD-0010` remains the home for broader renderer performance work, dirtying strategy, and low-level primitive optimization policy. -- Primitive optimization carried out under that thread must respect the normative separation established here. - -## Referencias - -- [AGD-0028-deferred-overlay-and-primitive-composition.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md) -- [AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md) -- [DEC-0014-frame-composer-render-integration.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md) -- [DEC-0015-frame-composer-public-syscall-surface.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md) - -## Propagacao Necessaria - -- A new implementation plan MUST be created before code changes. -- That plan MUST cover: - - deferred overlay/debug ownership outside `FrameComposer`; - - runtime frame-end ordering changes; - - no-scene path parity; - - spec/documentation updates for `gfx.*` primitive semantics. -- The implementation plan MUST NOT reopen the ownership boundary accepted here. - -## Revision Log - -- 2026-04-18: Initial accepted decision from `AGD-0028`. -- 2026-04-18: Linked implementation plan family `PLN-0026` through `PLN-0029`. diff --git a/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md b/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md deleted file mode 100644 index e7d3ff62..00000000 --- a/discussion/workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -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`. -- Preserve scene-layer metadata naming aligned with the world path contract, including `parallax_factor` as the canonical per-layer camera multiplier field. -- 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. -- Do not introduce any fixed `16x16` assumption into owned state; tile-size-sensitive behavior must derive from bound scene metadata. - -**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`. -- Construction and owned state shape do not encode `16x16` as an implicit world-path invariant. - -### 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. -- Encoding `16x16` into the initial owned-state shape would create a contract violation that later plans would have to unwind. diff --git a/discussion/workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md b/discussion/workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md deleted file mode 100644 index c101524e..00000000 --- a/discussion/workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -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. diff --git a/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md b/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md deleted file mode 100644 index 1b83928d..00000000 --- a/discussion/workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -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` retained inside `FrameComposer`. -The same decision also requires `FrameComposer` to remain tile-size agnostic and to preserve canonical per-layer `tile_size`, including `8x8`. -For the scene-layer motion contract, this plan treats `parallax_factor` as the canonical field name for the per-layer camera multiplier. - -## 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` -- 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. -- Any initialization must derive layer math from the bound scene tile sizes instead of assuming `16x16`. -- Any layer-camera math or related contract references must use `parallax_factor` terminology rather than generic `motion` naming. - -**File(s):** -- `crates/console/prometeu-drivers/src/frame_composer.rs` - -## Test Requirements - -### Unit Tests -- bind stores `scene_bank_id + Arc`. -- 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. -- bind/unbind remains valid for scenes whose layers use `8x8` tiles. -- scene binding and camera-facing contracts preserve `parallax_factor` as the canonical layer field name. - -### 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. -- [ ] Scene bind/cache/resolver setup preserves canonical per-layer tile sizes, including `8x8`. -- [ ] Scene-layer camera multiplier naming is aligned on `parallax_factor`. - -## 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. diff --git a/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md b/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md deleted file mode 100644 index 925517e8..00000000 --- a/discussion/workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -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. -`DEC-0014` also requires the world path to remain tile-size agnostic, with explicit support for `8x8`, `16x16`, and `32x32` scene-layer tile sizes. -For per-layer camera scaling, this plan treats `parallax_factor` as the canonical scene-layer field name. - -## 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. -- Ensure resolver and refresh math follow the bound layer `tile_size` values rather than any fixed `16x16` default. -- Ensure per-layer camera math is expressed through `parallax_factor` naming in the resolver/cache path. - -**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` -- World rendering must remain valid when the active scene uses `8x8` tiles. - -**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` - - active-scene composition with `8x8` tile-size layers - -**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`. -- `render_frame()` with an `8x8` scene uses resolver/cache math derived from layer tile size rather than a `16x16` assumption. -- Resolver/cache-facing tests use `parallax_factor` terminology for per-layer camera scaling. - -### Integration Tests -- scene bind + camera set + sprite emission + `render_frame()` produces the expected composed frame. -- scene bind + camera set + `8x8` scene + `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. -- [ ] The world path is explicitly covered for `8x8` scenes without `16x16`-specific assumptions. -- [ ] Resolver/cache/frame-path terminology is aligned on `parallax_factor` for scene-layer camera scaling. - -## 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. -- If tests cover only `16x16`, a latent compositor regression against canonical `8x8` scenes can ship unnoticed. diff --git a/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md b/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md deleted file mode 100644 index 00edec77..00000000 --- a/discussion/workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -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. -The same decision also requires the new canonical path to preserve scene-layer tile sizes such as `8x8`, not just `16x16`. - -## 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 - - active-scene frames remain valid for canonical `8x8` scenes - - 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. -- runtime tick path remains valid when the active scene uses `8x8` tiles. - -### 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. -- [ ] Regression coverage includes the canonical `8x8` world-path case. - -## 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. -- Migrating callsites without `8x8` regression coverage can falsely validate only the legacy `16x16` path. diff --git a/discussion/workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md b/discussion/workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md deleted file mode 100644 index 34916ad3..00000000 --- a/discussion/workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -id: PLN-0022 -ticket: frame-composer-public-syscall-surface -title: Plan - Composer Syscall Domain and Spec Propagation -status: accepted -created: 2026-04-17 -completed: -tags: [gfx, runtime, syscall, abi, spec, isa-core, frame-composer] ---- - -## Objective - -Introduce the canonical `composer.*` syscall domain, define `ComposerOpStatus`, and propagate the new public contract through the canonical spec, ABI documentation, and `ISA_CORE` artifacts where affected. - -## Background - -`DEC-0015` locks the public orchestration surface on `composer.*`, requires `ComposerOpStatus` for mutating composer-domain calls, and requires propagation beyond code into canonical spec, ABI-facing documentation, and `ISA_CORE` where the public syscall surface is described normatively. - -## Scope - -### Included -- add the `composer` syscall domain and ids -- define `ComposerOpStatus` -- remove `gfx.set_sprite(...)` from the public ABI contract -- update canonical spec documentation for the new public surface -- update ABI-facing documentation and `ISA_CORE` wherever the public syscall contract is described - -### Excluded -- runtime dispatch implementation -- cartridge and stress program migration -- final repository-wide CI execution - -## Execution Steps - -### Step 1 - Define the public `composer` syscall contract - -**What:** -Add the new canonical public syscall surface to the HAL syscall contract. - -**How:** -- Extend the syscall enum, registry, metadata, and resolver with a new `composer` domain. -- Allocate explicit syscall ids for: - - `composer.bind_scene` - - `composer.unbind_scene` - - `composer.set_camera` - - `composer.emit_sprite` -- Remove `gfx.set_sprite` from the public syscall contract and registry. -- Keep syscall metadata explicit for arg/ret slots and capability requirements. - -**File(s):** -- `crates/console/prometeu-hal/src/syscalls.rs` -- `crates/console/prometeu-hal/src/syscalls/domains/*` -- `crates/console/prometeu-hal/src/syscalls/registry.rs` -- `crates/console/prometeu-hal/src/syscalls/resolver.rs` - -### Step 2 - Introduce `ComposerOpStatus` - -**What:** -Create the status family for composer-domain mutating operations. - -**How:** -- Define a `ComposerOpStatus` type in HAL with explicit operational states needed by: - - scene binding - - scene unbinding - - sprite emission -- Ensure the enum is semantically composer-domain specific rather than a rename wrapper around `GfxOpStatus`. -- Update public API references so composer syscalls return `ComposerOpStatus` where required by `DEC-0015`. - -**File(s):** -- `crates/console/prometeu-hal/src/*` -- any shared status exports used by runtime/VM code - -### Step 3 - Propagate the contract into spec, ABI docs, and `ISA_CORE` - -**What:** -Update normative documentation so the public contract no longer describes legacy `gfx.set_sprite`. - -**How:** -- Identify canonical spec files that describe VM graphics/composition syscalls. -- Replace public references to legacy sprite orchestration with `composer.*`. -- Update ABI-facing docs to pin: - - namespace - - names - - arg order - - return semantics -- Update `ISA_CORE` if and where it references the affected syscall surface. -- Keep published spec content in English per repository policy. - -**File(s):** -- canonical spec location(s) -- ABI contract documentation -- `ISA_CORE` artifact(s) if affected - -## Test Requirements - -### Unit Tests -- syscall registry tests pin the new `composer.*` entries and reject removed legacy identities -- `ComposerOpStatus` values are pinned where public return semantics are asserted - -### Integration Tests -- declared syscall resolution accepts `composer.*` declarations and rejects removed `gfx.set_sprite` - -### Manual Verification -- inspect canonical spec, ABI docs, and `ISA_CORE` references to confirm the public contract matches `DEC-0015` - -## Acceptance Criteria - -- [ ] The public syscall registry exposes `composer.bind_scene`, `composer.unbind_scene`, `composer.set_camera`, and `composer.emit_sprite`. -- [ ] `ComposerOpStatus` exists as the canonical status family for composer-domain mutating syscalls. -- [ ] `gfx.set_sprite` is removed from the public ABI contract. -- [ ] Canonical spec documentation is updated to describe `composer.*`. -- [ ] ABI-facing docs and `ISA_CORE` are updated wherever the affected public surface is documented. - -## Dependencies - -- Source decision: `DEC-0015` - -## Risks - -- Missing a normative doc location would leave the code and published contract divergent. -- Reusing `GfxOpStatus` semantics by accident would weaken the service-boundary separation required by `DEC-0015`. -- Removing the legacy syscall contract incompletely could leave resolver or ABI ambiguity behind. diff --git a/discussion/workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md b/discussion/workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md deleted file mode 100644 index 70999ee8..00000000 --- a/discussion/workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -id: PLN-0023 -ticket: frame-composer-public-syscall-surface -title: Plan - Composer Runtime Dispatch and Legacy Removal -status: accepted -created: 2026-04-17 -completed: -tags: [runtime, syscall, frame-composer, dispatch, migration] ---- - -## Objective - -Route the new public `composer.*` syscalls through `FrameComposer`, remove legacy `gfx.set_sprite` handling, and align runtime-side operational behavior with `DEC-0015`. - -## Background - -`DEC-0015` closes the public contract around `composer.*` and requires that `gfx.set_sprite` be removed completely rather than kept as a compatibility shim. The internal `FrameComposer` ownership model already exists from `DEC-0014` and plans `PLN-0017` through `PLN-0021`. - -## Scope - -### Included -- runtime syscall dispatch for `composer.*` -- operational mapping from syscall args to `FrameComposer` -- removal of legacy `gfx.set_sprite` runtime handling -- runtime-facing tests for composer-domain behavior - -### Excluded -- spec and ABI doc propagation -- cartridge/tooling migration -- final `make ci` closure - -## Execution Steps - -### Step 1 - Add runtime dispatch for `composer.*` - -**What:** -Teach VM runtime dispatch to call `FrameComposer` through the new public contract. - -**How:** -- Add dispatch arms for: - - `composer.bind_scene` - - `composer.unbind_scene` - - `composer.set_camera` - - `composer.emit_sprite` -- Parse arguments exactly as pinned by the HAL metadata. -- Return `ComposerOpStatus` for mutating composer-domain syscalls. - -**File(s):** -- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` -- any adjacent runtime helpers - -### Step 2 - Map operational outcomes cleanly onto `ComposerOpStatus` - -**What:** -Make runtime failures and normal outcomes reflect the new composer-domain status model. - -**How:** -- Bind runtime-side operational checks to status outcomes such as: - - scene bank unavailable - - bank invalid - - argument range invalid - - layer invalid - - sprite overflow if surfaced operationally -- Keep non-fatal overflow behavior aligned with `DEC-0015`. - -**File(s):** -- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` -- `crates/console/prometeu-hal/src/*` as needed for shared status meaning - -### Step 3 - Remove legacy `gfx.set_sprite` runtime support - -**What:** -Delete the old public runtime path for slot-style sprite submission. - -**How:** -- Remove dispatch support for `gfx.set_sprite`. -- Remove runtime assumptions about `active`, caller-provided indices, and legacy sprite ABI shape. -- Keep no private compatibility hook behind the public API. - -**File(s):** -- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` -- adjacent tests and public syscall references - -## Test Requirements - -### Unit Tests -- runtime dispatch returns `ComposerOpStatus` for bind, unbind, and emit operations -- `composer.set_camera` stores the minimal V1 camera coordinates correctly - -### Integration Tests -- a VM/runtime test can bind a scene, set camera, emit a sprite, reach `FRAME_SYNC`, and render through the canonical frame path -- public runtime behavior rejects removed `gfx.set_sprite` declarations/calls - -### Manual Verification -- inspect dispatch code to confirm all public orchestration now routes through `FrameComposer` rather than a legacy `gfx` sprite syscall path - -## Acceptance Criteria - -- [ ] Runtime dispatch supports all canonical `composer.*` syscalls. -- [ ] Mutating composer-domain calls return `ComposerOpStatus`. -- [ ] `gfx.set_sprite` is removed from runtime public handling. -- [ ] Runtime tests cover scene bind, camera set, sprite emit, and frame rendering through the public path. - -## Dependencies - -- Depends on `PLN-0022` -- Source decision: `DEC-0015` - -## Risks - -- Removing legacy handling before all runtime references are migrated can strand tests or bytecode fixtures. -- Poor `ComposerOpStatus` mapping could collapse useful operational distinctions into generic failures. diff --git a/discussion/workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md b/discussion/workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md deleted file mode 100644 index 1e5fb3a1..00000000 --- a/discussion/workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -id: PLN-0024 -ticket: frame-composer-public-syscall-surface -title: Plan - Composer Cartridge, Tooling, and Regression Migration -status: accepted -created: 2026-04-17 -completed: -tags: [runtime, bytecode, tooling, stress, regression, frame-composer] ---- - -## Objective - -Migrate bytecode declarations, cartridges, stress tooling, and regression coverage from legacy public sprite orchestration to the canonical `composer.*` surface. - -## Background - -`DEC-0015` requires the new public composer-domain ABI to land without leaving `gfx.set_sprite` as a fallback. That means the migration must cover the generated bytecode, test cartridges, and stress tooling that still assume the old public contract. - -## Scope - -### Included -- bytecode declaration updates for `composer.*` -- cartridge and stress generator migration -- regression coverage for the public composer-domain path -- removal of legacy syscall usage from test and tooling surfaces - -### Excluded -- canonical spec propagation -- runtime dispatch implementation -- final repository-wide CI closure - -## Execution Steps - -### Step 1 - Migrate declared syscall users and fixtures - -**What:** -Update code and fixtures that declare public syscalls so they target `composer.*`. - -**How:** -- Replace legacy public sprite syscall declarations with composer-domain declarations. -- Update ABI expectations in bytecode-related tests and fixtures. -- Ensure removal of `gfx.set_sprite` is reflected in any declaration validation snapshots. - -**File(s):** -- bytecode tests and fixtures -- syscall declaration users across runtime and tools - -### Step 2 - Migrate stress and cartridge tooling - -**What:** -Make the stress cartridge and related generators exercise the canonical public frame path. - -**How:** -- Update `pbxgen-stress` and any cartridge generators to declare and call `composer.*`. -- Replace legacy sprite-path usage with `composer.emit_sprite`. -- Add scene bind and camera usage where needed so the stress path reaches the real canonical pipeline. - -**File(s):** -- `crates/tools/pbxgen-stress/src/*` -- `test-cartridges/stress-console/*` -- related scripts such as `scripts/run-stress.sh` - -### Step 3 - Expand regression coverage around the public path - -**What:** -Lock the new public orchestration contract with regression tests. - -**How:** -- Add tests that cover: - - composer-domain declaration resolution - - public bind/unbind/camera/emit behavior - - scene rendering through the public path - - stress/tooling integration using `composer.*` -- Ensure no regression fixture still relies on removed `gfx.set_sprite`. - -**File(s):** -- runtime tests -- HAL syscall tests -- tooling tests where available - -## Test Requirements - -### Unit Tests -- bytecode and syscall declaration tests pin `composer.*` names and slot counts - -### Integration Tests -- stress or cartridge-facing tests exercise scene bind, camera set, and sprite emit through `composer.*` -- regression fixtures fail if `gfx.set_sprite` is reintroduced - -### Manual Verification -- inspect generated stress cartridge declarations and program behavior to confirm the public path is truly composer-domain based - -## Acceptance Criteria - -- [ ] Bytecode declarations and fixtures use `composer.*` instead of legacy public sprite orchestration. -- [ ] Stress tooling and test cartridges exercise the canonical public `FrameComposer` path. -- [ ] Regression coverage protects against fallback to `gfx.set_sprite`. - -## Dependencies - -- Depends on `PLN-0022` and `PLN-0023` -- Source decision: `DEC-0015` - -## Risks - -- Partial cartridge/tooling migration could leave the repository with hidden legacy public ABI usage. -- Stress tooling may appear to pass while still missing scene/camera coverage if it only migrates sprite calls. diff --git a/discussion/workflow/plans/PLN-0025-final-ci-validation-and-polish.md b/discussion/workflow/plans/PLN-0025-final-ci-validation-and-polish.md deleted file mode 100644 index fd510d0d..00000000 --- a/discussion/workflow/plans/PLN-0025-final-ci-validation-and-polish.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -id: PLN-0025 -ticket: frame-composer-public-syscall-surface -title: Plan - Final CI Validation and Polish -status: accepted -created: 2026-04-17 -completed: -tags: [ci, validation, regression, polish] ---- - -## Objective - -Run the final repository validation path, including `make ci`, and perform the last compatibility, formatting, lint, and regression fixes required to close the composer-domain migration cleanly. - -## Background - -`DEC-0015` requires a coordinated migration across ABI, runtime, tooling, cartridges, spec, and documentation. After the implementation plans land, the repository still needs a final closure pass so no residual breakage survives in formatting, linting, tests, generated artifacts, or CI expectations. - -## Scope - -### Included -- final repository validation with `make ci` -- fixups required by formatting, lint, tests, snapshots, or generated artifacts -- final consistency pass across migrated files - -### Excluded -- introducing new contract changes beyond `DEC-0015` -- reopening ABI or service-boundary decisions - -## Execution Steps - -### Step 1 - Run the final validation entrypoint - -**What:** -Execute the repository’s final CI validation path. - -**How:** -- Run `make ci` after `PLN-0022`, `PLN-0023`, and `PLN-0024` are complete. -- Capture failures from formatting, lint, tests, coverage setup, generation steps, or artifact drift. - -**File(s):** -- repository-wide validation entrypoints - -### Step 2 - Apply closure fixes without reopening scope - -**What:** -Resolve residual breakage surfaced by final validation. - -**How:** -- Fix formatting and lint issues. -- Update snapshots or generated artifacts only where the migrated public contract requires it. -- Repair any remaining tests or documentation references that fail under `make ci`. -- Do not widen scope beyond the accepted composer-domain migration. - -**File(s):** -- any files directly implicated by final validation failures - -### Step 3 - Confirm final repository consistency - -**What:** -Leave the migration in a stable publishable state. - -**How:** -- Re-run `make ci` until it passes cleanly. -- Verify no legacy public `gfx.set_sprite` usage remains in code, tests, tooling, or docs. -- Confirm the worktree reflects only intended migration changes. - -**File(s):** -- repository-wide - -## Test Requirements - -### Unit Tests -- whatever unit coverage is exercised by `make ci` must remain green - -### Integration Tests -- repository integration coverage under `make ci` must pass after the migration - -### Manual Verification -- inspect the tree for residual `gfx.set_sprite` references and incomplete composer-domain propagation - -## Acceptance Criteria - -- [ ] `make ci` passes after the composer-domain migration family lands. -- [ ] Final fixups do not reopen contract scope beyond `DEC-0015`. -- [ ] No residual public `gfx.set_sprite` usage remains in the repository. - -## Dependencies - -- Depends on `PLN-0022`, `PLN-0023`, and `PLN-0024` -- Source decision: `DEC-0015` - -## Risks - -- If this final closure pass is skipped, small residual regressions can survive across formatting, lint, or generated artifacts even when the core implementation is correct. -- Late fixes can accidentally widen scope unless kept strictly bounded to validation fallout. diff --git a/discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md b/discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md deleted file mode 100644 index ab33e3d4..00000000 --- a/discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -id: PLN-0026 -ticket: deferred-overlay-and-primitive-composition -title: Plan - GFX Overlay Contract and Spec Propagation -status: accepted -created: 2026-04-18 -completed: -tags: [gfx, runtime, spec, overlay, primitives, hud] ---- - -## Objective - -Propagate `DEC-0016` into the canonical specs and internal contracts so `gfx.*` primitives are defined as deferred final overlay/debug operations outside `FrameComposer`. - -## Background - -`DEC-0016` locks a new semantic boundary: - -- `FrameComposer` remains the owner of canonical game-frame composition; -- `gfx.*` primitives and `draw_text(...)` become deferred final overlay/debug operations; -- that overlay lives outside `FrameComposer` and is drained after `hud_fade`. - -Execution must start by updating the normative contract before implementation changes spread through runtime and drivers. - -## Scope - -### Included -- update canonical runtime/gfx spec text to describe deferred overlay semantics -- update any ABI-facing or developer-facing docs that still imply direct stable writes to `back` -- align local contract comments and module docs where they currently imply immediate-write semantics as the stable model - -### Excluded -- implementation of the overlay subsystem -- runtime frame-end integration -- final repository-wide CI - -## Execution Steps - -### Step 1 - Update canonical graphics/runtime documentation - -**What:** -Publish the new semantic contract for `gfx.*` primitives. - -**How:** -- Update the canonical runtime/gfx spec so `gfx.draw_text(...)` and peer primitives are described as deferred final overlay/debug operations. -- State explicitly that primitives are not part of canonical scene/sprite/HUD composition. -- State the ordering rule that overlay/debug is drained after `hud_fade`. -- Ensure the no-scene and scene-bound paths are described consistently. - -**File(s):** -- canonical runtime/gfx spec files under `docs/specs/runtime/` - -### Step 2 - Align implementation-facing contract text - -**What:** -Remove stale implementation comments that imply immediate stable writes to the framebuffer. - -**How:** -- Inspect module-level comments and trait docs in `hal`, `drivers`, and runtime code for language that now contradicts `DEC-0016`. -- Update only the contract-bearing comments and docs that materially affect maintenance and implementation clarity. - -**File(s):** -- `crates/console/prometeu-hal/src/gfx_bridge.rs` -- `crates/console/prometeu-drivers/src/gfx.rs` -- `crates/console/prometeu-drivers/src/frame_composer.rs` -- runtime-adjacent modules where frame ordering is described - -## Test Requirements - -### Unit Tests -- none required for pure doc propagation - -### Integration Tests -- none required for pure doc propagation - -### Manual Verification -- inspect the updated spec and local contract comments to confirm they no longer describe primitives as stable direct writes to `back` - -## Acceptance Criteria - -- [ ] Canonical spec text describes `gfx.*` primitives as deferred final overlay/debug operations. -- [ ] The spec states that overlay/debug is outside `FrameComposer`. -- [ ] The spec states that overlay/debug is drained after `hud_fade`. -- [ ] Local implementation-facing contract comments no longer imply immediate-write semantics as the stable model. - -## Dependencies - -- Source decision: `DEC-0016` - -## Risks - -- Missing a normative doc location would leave code and published contract divergent. -- Over-editing local comments could unintentionally restate design choices outside the scope of `DEC-0016`. diff --git a/discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md b/discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md deleted file mode 100644 index e746e8fb..00000000 --- a/discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -id: PLN-0027 -ticket: deferred-overlay-and-primitive-composition -title: Plan - Deferred GFX Overlay Subsystem -status: accepted -created: 2026-04-18 -completed: -tags: [gfx, runtime, overlay, primitives, text, drivers] ---- - -## Objective - -Introduce a dedicated deferred overlay/debug subsystem for `gfx.*` primitives outside `FrameComposer`, with command capture for `draw_text(...)` and the primitive family selected for the first migration. - -## Background - -`DEC-0016` requires primitive/text overlay ownership to remain outside `FrameComposer` while still allowing shared raster helpers and low-level optimizations internally. The new subsystem must preserve semantic separation from scene/sprite/HUD composition. - -## Scope - -### Included -- introduce an overlay/debug command queue or equivalent subsystem outside `FrameComposer` -- route `gfx.draw_text(...)` into deferred command capture instead of stable direct framebuffer writes -- route the chosen V1 primitive family into the same deferred overlay/debug path -- keep raster helper reuse allowed without merging semantic ownership - -### Excluded -- runtime frame-end sequencing -- no-scene/scene parity tests at the runtime level -- final repository-wide CI - -## Execution Steps - -### Step 1 - Define overlay/debug state ownership in drivers - -**What:** -Create the subsystem that owns deferred `gfx.*` overlay/debug commands. - -**How:** -- Add a dedicated owner adjacent to `Gfx`/`Hardware`, but not inside `FrameComposer`. -- Define the minimal command model required for V1 operations. -- Keep the subsystem screen-space and explicitly pipeline-agnostic relative to `composer.*`. - -**File(s):** -- `crates/console/prometeu-drivers/src/*` -- `crates/console/prometeu-hal/src/*` if bridge traits need extension - -### Step 2 - Route text and selected primitives into deferred capture - -**What:** -Stop treating text/primitives as stable direct writes. - -**How:** -- Change `gfx.draw_text(...)` to enqueue deferred overlay/debug work. -- Migrate the selected V1 primitive set into the same deferred path. -- Keep any remaining unmigrated primitives either explicitly out of scope or routed consistently if they are already part of the accepted V1 set. -- Preserve internal raster helper reuse where useful. - -**File(s):** -- `crates/console/prometeu-drivers/src/gfx.rs` -- runtime dispatch call sites that submit `gfx.*` primitives - -### Step 3 - Add local driver-level tests for deferred capture semantics - -**What:** -Prove that overlay/debug commands are captured separately from game composition state. - -**How:** -- Add tests that assert text/primitives do not need direct stable writes to `back` to survive until overlay drain. -- Add tests that assert the overlay owner is independent from `FrameComposer` state. - -**File(s):** -- `crates/console/prometeu-drivers/src/gfx.rs` -- new or existing driver test modules - -## Test Requirements - -### Unit Tests -- command capture tests for `draw_text(...)` -- tests for each migrated V1 primitive class -- tests proving overlay/debug state is owned outside `FrameComposer` - -### Integration Tests -- none in this plan; runtime-level ordering is covered by the next plan - -### Manual Verification -- inspect driver ownership boundaries to confirm `FrameComposer` does not gain overlay/debug state - -## Acceptance Criteria - -- [ ] A dedicated deferred overlay/debug subsystem exists outside `FrameComposer`. -- [ ] `gfx.draw_text(...)` is captured as deferred overlay/debug work. -- [ ] The selected V1 primitive family is captured through the same subsystem. -- [ ] Driver-level tests prove overlay/debug state is operationally separate from canonical game composition state. - -## Dependencies - -- Source decision: `DEC-0016` -- Prefer to execute after `PLN-0026` - -## Risks - -- Accidentally reusing `FrameComposer` storage or state would violate the accepted ownership boundary. -- Migrating only part of the primitive family without explicit scoping could create inconsistent semantics across `gfx.*`. diff --git a/discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md b/discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md deleted file mode 100644 index 3fadea8e..00000000 --- a/discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -id: PLN-0028 -ticket: deferred-overlay-and-primitive-composition -title: Plan - Runtime Frame-End Overlay Integration and Parity -status: accepted -created: 2026-04-18 -completed: -tags: [runtime, overlay, frame-composer, no-scene, regression, stress] ---- - -## Objective - -Integrate deferred overlay/debug draining into the runtime frame-end sequence so scene-bound and no-scene frames both present the same final `gfx.*` primitive behavior after `hud_fade`. - -## Background - -After `PLN-0027`, the overlay/debug subsystem will exist but still needs to be drained in the correct place relative to `FrameComposer.render_frame()`, fades, and present/present-adjacent behavior. This plan closes the observable runtime semantics required by `DEC-0016`. - -## Scope - -### Included -- runtime frame-end ordering changes -- scene-bound and no-scene parity -- regression coverage for overlay visibility above the canonical game frame -- stress-cartridge adjustments if needed to prove text/primitives now survive frame composition - -### Excluded -- broad renderer optimization work -- final repository-wide CI - -## Execution Steps - -### Step 1 - Insert overlay/debug drain into the frame-end path - -**What:** -Drain deferred overlay/debug after canonical game composition is complete. - -**How:** -- Update the runtime frame-end path so overlay/debug drain occurs after: - - `FrameComposer.render_frame()` - - `scene_fade` - - `hud_fade` -- Ensure the same ordering is respected in the no-scene path. - -**File(s):** -- `crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs` -- `crates/console/prometeu-drivers/src/hardware.rs` -- `crates/console/prometeu-drivers/src/gfx.rs` -- any bridge traits needed by the runtime/hardware path - -### Step 2 - Add runtime and driver regressions for final visual ordering - -**What:** -Lock the new visible behavior. - -**How:** -- Add tests proving `gfx.draw_text(...)` remains visible after scene-backed frame composition. -- Add tests proving the same behavior with no scene bound. -- Add tests proving overlay/debug sits above the canonical game frame rather than being erased by it. - -**File(s):** -- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs` -- driver-level render tests where helpful - -### Step 3 - Update stress/integration fixtures if needed - -**What:** -Restore or improve stress scenarios that rely on visible text/primitives. - -**How:** -- Update `pbxgen-stress` or related stress fixtures so text/primitives are once again a valid visible overlay signal. -- Keep the stress focused on the new model rather than reintroducing obsolete immediate-write assumptions. - -**File(s):** -- `crates/tools/pbxgen-stress/src/lib.rs` -- `test-cartridges/stress-console/*` - -## Test Requirements - -### Unit Tests -- local ordering tests where runtime integration depends on helper sequencing - -### Integration Tests -- runtime tests for scene-bound overlay/debug visibility -- runtime tests for no-scene parity -- stress/tooling validation that text or primitives are visible again as final overlay/debug - -### Manual Verification -- run the stress path and visually confirm overlay/debug survives on top of scene/sprites after frame composition - -## Acceptance Criteria - -- [ ] The runtime drains deferred overlay/debug after canonical game composition and after `hud_fade`. -- [ ] Scene-bound and no-scene paths expose the same overlay/debug semantics. -- [ ] Regression tests prove `draw_text(...)` is no longer erased by scene-backed frame composition. -- [ ] Stress/integration fixtures reflect the new final-overlay semantics where applicable. - -## Dependencies - -- Source decision: `DEC-0016` -- Depends on `PLN-0027` - -## Risks - -- If fades are still applied after overlay/debug drain, the visible contract will contradict `DEC-0016`. -- Incomplete parity between scene-bound and no-scene paths would leave runtime behavior mode-dependent. diff --git a/discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md b/discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md deleted file mode 100644 index 085d83c2..00000000 --- a/discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -id: PLN-0029 -ticket: deferred-overlay-and-primitive-composition -title: Plan - Final Overlay CI Validation and Polish -status: accepted -created: 2026-04-18 -completed: -tags: [ci, overlay, runtime, gfx, validation] ---- - -## Objective - -Run the final repository validation path for the deferred overlay/debug migration and perform the last compatibility, formatting, lint, and regression fixes required to close the thread cleanly. - -## Background - -`DEC-0016` changes visible runtime semantics and touches both specs and code paths around frame composition. A dedicated final-validation plan is needed so the implementation family can close on a clean CI signal rather than leaving integration fallout for later. - -## Scope - -### Included -- full-tree formatting, lint, and test validation -- stress-path smoke validation after overlay integration -- final cleanup fixes required to satisfy CI - -### Excluded -- new feature work outside the accepted overlay/debug migration - -## Execution Steps - -### Step 1 - Run focused validation before full CI - -**What:** -Catch local fallout in the touched areas before the full repository pass. - -**How:** -- Run targeted tests for drivers, runtime, and `pbxgen-stress`. -- Inspect touched files for stale immediate-write assumptions or missed contract updates. - -**File(s):** -- touched files from `PLN-0026` through `PLN-0028` - -### Step 2 - Run final repository CI - -**What:** -Validate the migration end to end. - -**How:** -- Run the repository validation path, including `make ci`. -- Fix any final formatting, lint, test, or generated-fixture fallout caused by the overlay/debug migration. -- Do not widen scope beyond the accepted thread. - -**File(s):** -- repository-wide - -## Test Requirements - -### Unit Tests -- all relevant crate unit tests pass after the migration - -### Integration Tests -- runtime and stress/integration tests pass after the migration -- `make ci` passes - -### Manual Verification -- inspect the tree for residual direct-write assumptions or incomplete overlay propagation - -## Acceptance Criteria - -- [ ] Targeted validation passes for the touched drivers/runtime/stress areas. -- [ ] `make ci` passes after the deferred overlay/debug migration family lands. -- [ ] No residual contract mismatch remains between spec text and code behavior in the touched thread. - -## Dependencies - -- Source decision: `DEC-0016` -- Depends on `PLN-0026`, `PLN-0027`, and `PLN-0028` - -## Risks - -- Final CI may surface unrelated renderer assumptions that still expect immediate-write semantics. -- Generated cartridge fixtures may drift if regeneration is forgotten during earlier plans. -- 2.47.2