From c52f196b7aaa13e1025d984af5431dd221cb92a3 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 24 Apr 2026 10:56:14 +0100 Subject: [PATCH 1/8] SCENE BANL leaking slot ids to binary contract --- discussion/index.ndjson | 3 +- ...9-scene-bank-glyph-runtime-binding-leak.md | 193 ++++++++++++++++++ ...lyph-dependencies-must-bind-by-asset-id.md | 141 +++++++++++++ ...e-glyph-dependency-spec-and-wire-layout.md | 105 ++++++++++ ...lyph-runtime-resolution-and-fatal-paths.md | 141 +++++++++++++ ...ene-glyph-tooling-and-fixture-migration.md | 116 +++++++++++ 6 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 discussion/workflow/agendas/AGD-0029-scene-bank-glyph-runtime-binding-leak.md create mode 100644 discussion/workflow/decisions/DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md create mode 100644 discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md create mode 100644 discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md create mode 100644 discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index bc6b5c22..17137cf0 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":21,"PLN":40,"LSN":38,"CLSN":1}} +{"type":"meta","next_id":{"DSC":30,"AGD":30,"DEC":22,"PLN":43,"LSN":38,"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"}]} @@ -21,6 +21,7 @@ {"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-0029","status":"open","ticket":"scene-bank-glyph-runtime-binding-leak","title":"Scene Bank Glyph Runtime Binding Leak","created_at":"2026-04-24","updated_at":"2026-04-24","tags":["gfx","runtime","asset","scene","glyph","format","architecture"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-scene-bank-glyph-runtime-binding-leak.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-24"}],"decisions":[{"id":"DEC-0021","file":"DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md","status":"in_progress","created_at":"2026-04-24","updated_at":"2026-04-24","ref_agenda":"AGD-0029"}],"plans":[{"id":"PLN-0040","file":"PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0041","file":"PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0042","file":"PLN-0042-scene-glyph-tooling-and-fixture-migration.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]}],"lessons":[]} {"type":"discussion","id":"DSC-0014","status":"done","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0035","file":"lessons/DSC-0014-perf-vm-allocation-and-copy-pressure/LSN-0035-first-materialization-is-not-the-same-as-hot-path-copy-pressure.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]} {"type":"discussion","id":"DSC-0015","status":"abandoned","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"abandoned","created_at":"2026-03-27","updated_at":"2026-04-20","_override_reason":"User explicitly chose to close the discussion without decision because FS->memory copy for the program is already acceptable."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to abandon the discussion without creating a decision because FS->memory copy for the program is already acceptable."} {"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-0029-scene-bank-glyph-runtime-binding-leak.md b/discussion/workflow/agendas/AGD-0029-scene-bank-glyph-runtime-binding-leak.md new file mode 100644 index 00000000..c590fc23 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0029-scene-bank-glyph-runtime-binding-leak.md @@ -0,0 +1,193 @@ +--- +id: AGD-0029 +ticket: scene-bank-glyph-runtime-binding-leak +title: Scene Bank vazando glyph bank id runtime para o formato binario +status: accepted +created: 2026-04-24 +resolved: 2026-04-24 +decision: DEC-0021 +tags: [gfx, runtime, asset, scene, glyph, format, architecture] +--- + +## Contexto + +Hoje o payload binario `SCENE` carrega `glyph_bank_id` por layer e o runtime usa esse valor como chave operacional para resolver o `GlyphBank` residente durante o render. + +A cadeia atual eh: + +- o decoder de `SCENE` le `glyph_bank_id` do payload; +- `SceneLayer` preserva esse campo no modelo canonico carregado; +- `SceneViewportCache` copia o mesmo id para a cache; +- o renderer consulta `GlyphBankPoolAccess` por esse slot/id para desenhar a layer. + +Isso cria um acoplamento direto entre formato frio de asset e topologia de residencia runtime. A suspeita levantada eh que esse campo nao deveria existir no wire-format, porque a referencia correta ao glyph bank deveria ser resolvida apenas pelo runtime. + +## Problema + +O contrato atual mistura duas camadas que deveriam permanecer separadas: + +- o asset `SCENE` como descricao fria de composicao; +- o binding de slot/banco residente como estado operacional do runtime. + +Se `glyph_bank_id` realmente for um detalhe exclusivamente runtime, o formato binario atual esta errado e o runtime passou a depender de um identificador que nao deveria ser serializado nem publicado como parte do asset. + +Isso deixa perguntas abertas sobre: + +- como uma layer de cena passa a apontar para um glyph source sem carregar um slot runtime no payload; +- onde esse binding deve viver; +- como preload, install e bind de cena passam a cooperar sem reintroduzir ambiguidade. + +## Pontos Criticos + +- O renderer atual resolve a bank diretamente a partir de `layer_cache.glyph_bank_id`, entao qualquer correcao toca formato, modelo em memoria e caminho de render. +- O payload `SCENE` ja foi materializado em testes e tooling com esse byte no header da layer, entao a mudanca implica migracao/versionamento ou ruptura controlada. +- O contrato de asset management enfatiza slots como conceito runtime e resolve contexto de banco via `asset_table` e preload, o que conflita com `SCENE` carregar referencia operacional propria. +- Precisamos evitar uma "correcao" que apenas mova o mesmo acoplamento para outro nome sem restaurar a fronteira correta entre asset frio e binding runtime. + +## Opcoes + +### Opcao 1 - Manter `glyph_bank_id` dentro do formato `SCENE` + +- **Abordagem:** aceitar explicitamente que cada layer da cena referencia um slot/runtime glyph bank no proprio payload e documentar isso como contrato oficial. +- **Pros:** menor mudanca imediata; preserva compatibilidade com decoder, tests e tooling atuais. +- **Contras:** cristaliza o vazamento arquitetural; faz o asset depender de topologia runtime; dificulta relocacao, preload flexivel e reuso de cena em contextos diferentes. +- **Trade-off:** simples agora, mais rigido e mais fragil como contrato de longo prazo. + +### Opcao 2 - Remover o id do wire-format e resolver o binding por runtime ao ativar a cena + +- **Abordagem:** o payload `SCENE` deixa de carregar slot/id de glyph bank; a associacao layer -> fonte grafica passa a ser injetada por metadados de runtime, por preload derivado, ou por um binding explicito durante `bind_scene`. +- **Pros:** restaura a separacao asset frio vs residencia runtime; permite relocacao de slots e politica de preload sem contaminar o formato. +- **Contras:** exige definir uma nova fonte de verdade para o binding; impacta decoder, modelo de cena, renderer, specs e ferramentas. +- **Trade-off:** maior custo de migracao, mas fronteira arquitetural mais coerente. + +### Opcao 3 - Manter referencia serializada, mas trocar de slot runtime para identidade fria + +- **Abordagem:** o payload `SCENE` deixa de carregar `glyph_bank_id` runtime e passa a carregar uma referencia fria estavel por layer, como `asset_id` ou handle logico de glyph source; o runtime resolve essa identidade para slot residente quando a cena eh ativada. +- **Pros:** preserva auto-descricao da cena sem serializar topologia de residencia; reduz dependencia de slot fixo. +- **Contras:** ainda precisamos decidir se a cena deve conhecer assets frios diretamente; adiciona etapa de resolucao e falha operacional nova. +- **Trade-off:** melhor que serializar slot runtime, mas ainda pode acoplar demais o asset `SCENE` ao catalogo de assets. + +## Sugestao / Recomendacao + +Direcao escolhida nesta agenda: + +- seguir pela **Opcao 3** com identidade fria por `asset_id`; +- remover `glyph_bank_id` runtime do formato binario `SCENE`; +- fazer o `SCENE` carregar, por layer, o `asset_id` do glyph dependency; +- resolver `asset_id -> glyph slot` no momento de `bind_scene`; +- falhar operacionalmente se qualquer dependencia grafica da cena nao estiver comprometida em algum slot glyph; +- a ordem de carregamento dos assets nos bancos nao importa; +- **sem versionamento e sem compatibilidade retroativa por enquanto**. + +Motivo: + +- slot de glyph bank eh conceito runtime e nao deve viver no wire-format; +- `asset_id` eh identidade fria e estavel dentro do contrato de assets; +- bind-time resolution preserva a liberdade de preload/commit e evita acoplamento do asset `SCENE` a uma topologia fixa de slots; +- a falha operacional em `bind_scene` mantem o problema na fronteira correta de ativacao. + +Cravado nesta agenda: + +- `SceneBank` canonico nao deve carregar slot runtime de glyph; +- o payload `SCENE` deve declarar dependencias frias, nao residencia; +- a vinculacao layer -> glyph slot pertence ao runtime no momento de ativacao da cena; +- o runtime deve validar dependencias no `bind_scene`, mas nao deve persistir um snapshot resolvido como dado canonico da cena; +- no draw de cada layer, o runtime pode consultar novamente qual slot contem o `asset_id` referido pela cena; +- ausencia da dependencia em qualquer ponto relevante do fluxo deve produzir erro operacional. +- a resolucao `asset_id -> slot` fica interna nesta etapa; +- a resolucao interna deve ser suportada por indice reverso `asset_id -> slot`, nao depender apenas de scan linear dos slots; +- o payload `SCENE` deve aumentar a largura do campo para comportar `AssetId = i32`; +- cada layer publica sua propria dependencia glyph diretamente; +- falha de dependencia no caminho de draw/composition deve ser tratada como erro fatal da maquina, com log claro. +- falha de dependencia no `bind_scene` tambem deve ser tratada como erro fatal da maquina, com log claro. + +## Perguntas em Aberto + +## Criterio para Encerrar + +Esta agenda pode ser encerrada quando houver resposta clara e defendivel para: + +- como o `SCENE` publica dependencias por `asset_id`; +- como o runtime consulta `asset_id -> slot` durante bind e draw; +- qual representacao binaria carregara o `asset_id` por layer; +- como o contrato de erro fatal e log claro se aplica em bind e draw. + +## Discussao + +Estado inicial da investigacao: + +- ha vazamento real hoje no codigo e no tooling; +- nao encontrei decisao normativa registrada que feche explicitamente esse acoplamento; +- portanto o tema continua aberto e precisa de decisao arquitetural antes de qualquer correcao de spec ou codigo. + +Atualizacao de 2026-04-24: + +O caminho proposto pelo usuario parece tecnicamente viavel com a infraestrutura atual, embora ainda nao exista como contrato explicito. + +O que ja existe hoje: + +- o `AssetManager` mantem deduplicacao/residencia por `asset_id`; +- no commit/preload, cada slot fisico passa a registrar qual `asset_id` esta instalado nele; +- portanto o runtime ja possui as duas metades da informacao: + - `asset_id -> resident object`; + - `slot -> asset_id`. + +O que ainda nao existe: + +- uma API canonica `asset_id -> slot` para lookup de um asset ja comprometido em um banco; +- uma forma de `SceneBank` carregar dependencias por `asset_id` em vez de `glyph_bank_id`; +- um `bind_scene` capaz de validar todas as dependencias da cena e falhar operacionalmente se alguma nao estiver residente em slot algum. + +Leitura atual da proposta: + +- o payload `SCENE` carrega, por layer, o `asset_id` do glyph dependency; +- `composer.bind_scene(scene_slot)` continua selecionando a cena residente; +- durante o bind, o runtime resolve cada `asset_id` dependente para o slot glyph atualmente comprometido; +- durante o draw de cada layer, o runtime pode consultar novamente em qual slot o `asset_id` esta residente; +- a ordem de carregamento deixa de importar porque a resolucao eh feita contra o estado runtime no momento da operacao; +- se qualquer dependencia nao estiver carregada em algum slot glyph, `bind_scene` retorna falha operacional e nao ativa a cena. + +Essa direcao preserva a fronteira correta melhor do que o modelo atual: + +- o `SCENE` deixa de carregar topologia runtime; +- a cena continua auto-descritiva o suficiente para declarar suas dependencias frias; +- o runtime continua dono da residencia e da escolha de slot efetivo. + +Ponto de atencao: + +- essa direcao prefere nao manter snapshot resolvido como dado persistido da cena; +- portanto o contrato precisa dizer com clareza como o sistema reage se uma dependencia desaparecer entre bind e draw. + +Direcao aceita na discussao atual: + +- seguir com dependencia fria por `asset_id` no `SCENE`; +- resolver para slot glyph somente no `bind_scene`; +- permitir nova consulta `asset_id -> slot` tambem no draw das layers; +- nao expor lookup como API publica nesta etapa; +- manter indice reverso interno `asset_id -> slot` desde ja, para evitar acoplamento da solucao a scan linear de poucos slots; +- aumentar a largura do payload para carregar `AssetId = i32` no `SCENE`; +- tratar dependencia ausente no `bind_scene` como falha fatal da maquina, com log claro; +- tratar dependencia ausente no draw/composition como falha fatal da maquina, com log claro; +- nao fazer versionamento nem camada de compatibilidade nesta etapa. + +Resposta consolidada do usuario na discussao: + +- nao devemos materializar esse binding como dado resolvido persistente; +- devemos checar no bind e tambem quando formos desenhar a layer; +- a resolucao `asset_id -> slot` pode e deve evoluir para API publica futuramente, mas fica interna por enquanto; +- como essa resolucao tende a virar API publica, a base interna deve nascer como indice reverso e nao como mero loop sobre 16 slots; +- como `AssetId` canonico eh `i32`, o payload precisa aumentar a largura do campo serializado; +- cada layer carrega sua propria dependencia glyph; +- se a dependencia faltar ou nao puder ser resolvida no `bind_scene`, isso deve quebrar a maquina com log claro; +- se a dependencia sumir ou nao puder ser resolvida no caminho de draw/composition, isso tambem deve quebrar a maquina com log claro, porque nao ha como prosseguir corretamente sem as dependencias da cena. + +## Resolution + +Encerrada em 2026-04-24. + +Resolucao aceita: + +- `SCENE` deixa de carregar slot runtime de glyph e passa a carregar `AssetId` por layer; +- o runtime resolve `asset_id -> slot` via indice reverso interno; +- falta de dependencia fataliza a maquina tanto no `bind_scene` quanto no draw/composition, com log claro; +- nao ha versionamento nem compatibilidade retroativa nesta etapa. diff --git a/discussion/workflow/decisions/DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md b/discussion/workflow/decisions/DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md new file mode 100644 index 00000000..57c49b42 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md @@ -0,0 +1,141 @@ +--- +id: DEC-0021 +ticket: scene-bank-glyph-runtime-binding-leak +title: Scene Bank glyph dependencies MUST bind by asset_id, not runtime slot +status: in_progress +created: 2026-04-24 +accepted: 2026-04-24 +agenda: AGD-0029 +plans: [PLN-0040, PLN-0041, PLN-0042] +tags: [gfx, runtime, asset, scene, glyph, format, architecture] +--- + +## Status + +Accepted on 2026-04-24. + +## Contexto + +The current `SCENE` binary payload serializes `glyph_bank_id` per layer and the runtime uses that serialized value as the operational key to resolve the resident glyph bank during rendering. + +That design leaks runtime residency topology into a cold asset format. A scene asset is therefore coupled to whichever glyph slot layout happened to exist when the payload was authored, which violates the intended separation between: + +- cold asset description; +- runtime residency and slot assignment; +- frame-time composition. + +The repository discussion in `AGD-0029` concluded that this contract is incorrect and must be replaced. + +## Decisao + +The `SCENE` asset contract MUST stop serializing runtime glyph slot identifiers. + +From this decision forward: + +1. Each scene layer MUST declare its glyph dependency by `AssetId`. +2. `SceneBank` MUST NOT carry a runtime glyph slot id as part of its canonical loaded model. +3. The runtime MUST resolve `AssetId -> glyph slot` through runtime-owned residency state, not through scene payload bytes interpreted as slot ids. +4. The runtime MUST maintain an internal reverse index `AssetId -> slot` for committed glyph assets. +5. The lookup mechanism MAY become a public API later, but in this stage it MUST remain internal. +6. The `SCENE` payload field used for the glyph dependency MUST widen to carry canonical `AssetId = i32`. +7. The scene payload MUST keep the dependency attached directly to each layer; no shared dependency table is introduced in this decision. +8. No compatibility layer and no format version migration are introduced in this stage. + +## Rationale + +The previous contract was wrong at the boundary level, not only at the implementation level. + +Why this decision is correct: + +- runtime slot ids are operational state and MUST remain runtime-owned; +- `AssetId` is the stable cold identity already used by the asset system; +- resolving dependencies at runtime preserves freedom of preload, commit, relocation, and slot reuse; +- a reverse index gives the runtime the correct primitive for both internal use and possible future API exposure; +- keeping the dependency per layer is the lowest-risk migration path and avoids speculative structure growth. + +This decision deliberately prefers a clean contract over temporary compatibility because the current behavior is architecturally invalid. + +## Invariantes / Contrato + +### Canonical scene contract + +- A `SCENE` asset MUST describe glyph dependencies in cold asset terms only. +- A `SCENE` asset MUST NOT encode runtime slot topology. +- A scene layer MUST own exactly one glyph dependency reference. +- That dependency reference MUST be serialized as `AssetId` and MUST use `i32` width. + +### Runtime residency contract + +- The asset manager MUST own the authoritative mapping from committed glyph residency to slot. +- The runtime MUST maintain a reverse index from committed glyph `AssetId` to glyph slot. +- The reverse index MUST be updated whenever glyph preload or glyph commit changes slot ownership. +- The reverse index MUST be treated as internal runtime state in this stage. + +### Bind-time and draw-time contract + +- `bind_scene` MUST validate that every glyph dependency referenced by the target scene resolves to a currently committed glyph slot. +- If any dependency cannot be resolved during `bind_scene`, the machine MUST fail fatally and emit a clear log describing the missing dependency. +- Render-time composition MAY consult the reverse index again while drawing a layer instead of relying on a frozen slot snapshot. +- If a dependency becomes unresolved during draw/composition, the machine MUST fail fatally and emit a clear log describing the missing dependency. +- The runtime MUST NOT continue scene composition after a missing dependency is detected at bind time or draw time. + +### Error semantics + +- `ERROR_MISSING_DEPENDENCIES` is not part of the accepted contract. +- Missing scene glyph dependencies are not passive operational rejections in the accepted model. +- Missing scene glyph dependencies MUST escalate as fatal machine errors with explicit logging. + +### Non-goals for this decision + +- This decision does NOT expose the reverse lookup as a public API yet. +- This decision does NOT introduce payload compatibility with the old serialized `glyph_bank_id`. +- This decision does NOT introduce a dependency table shared across scene layers. +- This decision does NOT define the later public guest ABI for querying `asset_id -> slot`. + +## Impactos + +### Spec + +- The `SCENE` binary payload specification MUST be updated to replace per-layer runtime slot ids with per-layer `AssetId`. +- The payload layout MUST reflect widened field width for `AssetId = i32`. +- Runtime/spec text that implies scene glyph dependencies are operational status returns rather than fatal errors MUST be corrected. + +### Runtime + +- `SceneLayer` and related scene/cache structures MUST stop carrying canonical runtime glyph slot ids. +- The asset manager MUST maintain the reverse glyph residency index. +- `bind_scene` MUST validate scene glyph dependencies against the reverse index and fatalize on absence. +- The draw path MUST resolve or revalidate layer dependencies against runtime residency and fatalize on absence. + +### Host + +- Fatal machine logging for missing scene glyph dependencies MUST be clear enough to identify: + - the scene being bound or drawn; + - the layer involved; + - the missing glyph dependency `AssetId`. + +### Tooling + +- Scene encoders, packers, fixtures, and stress tools MUST serialize per-layer glyph dependency as `AssetId`, not runtime slot id. +- Existing tests and fixtures that publish `glyph_bank_id` in `SCENE` payloads MUST be rewritten. + +## Referencias + +- Agenda: `AGD-0029-scene-bank-glyph-runtime-binding-leak.md` +- Related runtime area: `DSC-0025` lesson on scene/canonical/cache separation +- Related asset identity context: `DSC-0018` lesson on asset load asset id int contract + +## Propagacao Necessaria + +- Update the canonical `SCENE` payload layout in specs. +- Update HAL scene types to reflect `AssetId` dependency semantics. +- Update asset decode/encode logic for `SCENE`. +- Introduce and maintain reverse glyph residency index in the asset manager. +- Update `FrameComposer` bind path and draw path to use runtime dependency resolution and fatal logging. +- Rewrite affected tests, fixtures, and tooling payload writers. +- Create an execution plan before spec/code changes. + +## Revision Log + +- 2026-04-24: Initial accepted decision derived from AGD-0029. +- 2026-04-24: Moved to in_progress after plan family creation (`PLN-0040`, `PLN-0041`, `PLN-0042`). diff --git a/discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md b/discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md new file mode 100644 index 00000000..0835ac0f --- /dev/null +++ b/discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md @@ -0,0 +1,105 @@ +--- +id: PLN-0040 +ticket: scene-bank-glyph-runtime-binding-leak +title: Scene glyph dependency spec and wire layout migration +status: review +created: 2026-04-24 +completed: +tags: [gfx, runtime, asset, scene, glyph, format, spec] +--- + +## Briefing + +Implement the specification-side portion of `DEC-0021` by rewriting the `SCENE` payload contract so that each scene layer serializes a glyph dependency by canonical `AssetId (i32)` instead of a runtime glyph slot id. This plan covers only normative docs and wire-layout definition. It does not implement runtime code. + +## Decisions de Origem + +- `DEC-0021` — Scene Bank glyph dependencies MUST bind by `asset_id`, not runtime slot. + +## Alvo + +- Canonical runtime specs under `docs/specs/runtime/`. +- Shared payload constants and comments under `crates/console/prometeu-hal/` only where needed to reflect the normative wire layout. + +## Escopo + +- Rewrite the `SCENE` payload description to replace per-layer runtime slot semantics with per-layer `AssetId`. +- Define widened field width for serialized scene glyph dependencies. +- Remove any spec language implying passive missing-dependency status for scene composition. +- Align related comments/constants with the new payload field width and meaning. + +## Fora de Escopo + +- Runtime bind/draw implementation. +- Reverse index implementation in `AssetManager`. +- Test fixture rewrites and tool migration. +- Any compatibility or versioning path for the old payload. + +## Plano de Execucao + +### Step 1 - Rewrite the normative `SCENE` payload contract + +**What:** +Define the scene-layer dependency field as `AssetId (i32)` and remove all remaining wording that treats scene glyph linkage as runtime slot topology. + +**How:** +Update the relevant `docs/specs/runtime` sections so the payload layout, field meaning, and operational consequences all match `DEC-0021`. State explicitly that the scene asset is cold-description only and MUST NOT encode runtime slot ids. + +**File(s):** +- `docs/specs/runtime/15-asset-management.md` +- any scene/gfx spec chapter that documents `SCENE` payload fields or scene dependency semantics. + +### Step 2 - Update shared HAL wire-layout constants and inline documentation + +**What:** +Bring shared constants and comments into sync with the widened scene dependency field. + +**How:** +Review `crates/console/prometeu-hal/src/asset.rs` and any nearby scene-related code comments. Update layer-header sizing constants and field comments if the payload header width changes from the previous single-byte dependency representation. + +**File(s):** +- `crates/console/prometeu-hal/src/asset.rs` +- neighboring scene payload comments when affected. + +### Step 3 - Tighten the published fatal-error semantics + +**What:** +Make the public documentation consistent with the accepted fatal behavior for missing scene glyph dependencies. + +**How:** +Remove proposed/legacy wording that frames missing dependencies as recoverable status returns for scene activation/composition. Describe the bind-time and draw-time dependency absence as fatal machine errors with clear logging expectations. + +**File(s):** +- `docs/specs/runtime/04-gfx-peripheral.md` +- `docs/specs/runtime/16-host-abi-and-syscalls.md` +- any other spec chapter that describes `composer.bind_scene`. + +## Criterios de Aceite + +- [ ] The published `SCENE` payload contract defines per-layer glyph dependency as `AssetId (i32)`. +- [ ] No normative spec text claims that `SCENE` serializes runtime glyph slot ids. +- [ ] No normative spec text retains `ERROR_MISSING_DEPENDENCIES` as the accepted contract for scene dependency failures. +- [ ] Shared HAL constants/comments reflect the revised payload layout where applicable. + +## Tests / Validacao + +### Unit Tests + +- Add or update compile-time/runtime assertions only if shared payload constants are exercised by existing tests. + +### Integration Tests + +- None in this plan; verification is doc/contract oriented. + +### Manual Verification + +- Review the updated spec text to ensure every `SCENE` glyph dependency reference uses `AssetId` language. +- Verify that the documented payload width matches `AssetId = i32`. +- Verify that fatal semantics for missing dependencies are stated consistently across spec chapters. + +## Riscos + +- Spec-only edits can drift from the runtime implementation if later plans are not executed promptly. +- Scene payload layout changes may require touching multiple docs; missing one will leave contradictory contract text. +- If HAL constants are updated here before runtime code, temporary compile or test fallout may appear in later plans and must be handled intentionally. + diff --git a/discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md b/discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md new file mode 100644 index 00000000..9bf062ce --- /dev/null +++ b/discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md @@ -0,0 +1,141 @@ +--- +id: PLN-0041 +ticket: scene-bank-glyph-runtime-binding-leak +title: Scene glyph runtime resolution, reverse index, and fatal paths +status: review +created: 2026-04-24 +completed: +tags: [gfx, runtime, asset, scene, glyph, render] +--- + +## Briefing + +Implement the runtime half of `DEC-0021`: remove canonical runtime glyph slot ids from scene types, widen scene dependency serialization to `AssetId (i32)`, introduce and maintain the internal reverse glyph residency index, and enforce fatal machine behavior when scene dependencies are missing at bind time or draw time. + +## Decisions de Origem + +- `DEC-0021` — Scene Bank glyph dependencies MUST bind by `asset_id`, not runtime slot. + +## Alvo + +- `crates/console/prometeu-hal/` +- `crates/console/prometeu-drivers/` +- `crates/console/prometeu-system/` only if runtime-visible behavior or tests require updates. + +## Escopo + +- Replace scene-layer glyph slot semantics with `AssetId` semantics in HAL/runtime types. +- Widen `SCENE` decode/encode logic to serialize `AssetId (i32)` per layer. +- Maintain an internal reverse index `AssetId -> glyph slot` for committed glyph assets. +- Enforce fatal handling with explicit logging when dependencies are missing during `bind_scene` or draw/composition. + +## Fora de Escopo + +- Public guest ABI for reverse lookup. +- Compatibility with old `glyph_bank_id` scene payloads. +- Tooling/stress-fixture migration beyond the runtime code needed to compile. + +## Plano de Execucao + +### Step 1 - Change canonical scene types to cold dependency identity + +**What:** +Remove canonical runtime glyph slot ids from scene-layer related types and replace them with glyph dependency `AssetId`. + +**How:** +Refactor `SceneLayer`, scene cache entries, viewport cache state, resolver helpers, and tests so the scene model stores dependency identity rather than slot identity. Preserve only the information needed for cold scene description plus runtime materialization. + +**File(s):** +- `crates/console/prometeu-hal/src/scene_layer.rs` +- `crates/console/prometeu-hal/src/scene_viewport_cache.rs` +- `crates/console/prometeu-hal/src/scene_viewport_resolver.rs` +- scene-related tests under `prometeu-hal`. + +### Step 2 - Update `SCENE` decode/encode path to `AssetId (i32)` + +**What:** +Rewrite the binary `SCENE` decoder and local encoders to read/write `AssetId` instead of single-byte glyph slot ids. + +**How:** +Adjust header width constants, field offsets, and validation in `AssetManager::decode_scene_bank_*` and any local test encoders. Ensure decode rejects malformed payloads under the new layout and that any helper fixture builder writes `i32` dependencies per layer. + +**File(s):** +- `crates/console/prometeu-drivers/src/asset.rs` +- any scene payload helpers under tests. + +### Step 3 - Introduce reverse glyph residency index + +**What:** +Add a runtime-owned reverse mapping from committed glyph `AssetId` to glyph slot. + +**How:** +Extend `AssetManager` so glyph preload, glyph commit, shutdown, and any slot replacement paths update both: +- the existing `slot -> asset_id` tracking; +- the new reverse `asset_id -> slot` index. + +Keep this mechanism internal in this stage. Remove stale entries when a slot is overwritten or cleared. + +**File(s):** +- `crates/console/prometeu-drivers/src/asset.rs` +- `crates/console/prometeu-hal/src/asset_bridge.rs` only if an internal-facing trait hook is required. + +### Step 4 - Enforce fatal dependency validation in `bind_scene` + +**What:** +Make scene activation fail fatally when any scene glyph dependency does not resolve to a committed glyph slot. + +**How:** +Update the `FrameComposer` bind path and any caller path so bind checks every layer dependency against the reverse index before activating the scene. Emit a clear fatal log naming the scene context, layer index, and missing dependency `AssetId`. + +**File(s):** +- `crates/console/prometeu-drivers/src/frame_composer.rs` +- `crates/console/prometeu-drivers/src/hardware.rs` +- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` if behavior shaping is needed. + +### Step 5 - Enforce fatal dependency validation in draw/composition + +**What:** +Make render-time scene composition fail fatally if a previously bindable dependency later becomes unresolved. + +**How:** +Update the draw path so each layer resolves or revalidates the current glyph slot for the layer dependency through the reverse index instead of trusting old scene slot data. If resolution fails, emit the same class of fatal log and abort machine progress. + +**File(s):** +- `crates/console/prometeu-drivers/src/gfx.rs` +- `crates/console/prometeu-drivers/src/frame_composer.rs` + +## Criterios de Aceite + +- [ ] No canonical scene type stores runtime glyph slot id as scene dependency state. +- [ ] `SCENE` decode/encode paths serialize per-layer dependency as `AssetId (i32)`. +- [ ] `AssetManager` maintains a reverse glyph index updated by preload and commit. +- [ ] `bind_scene` fatalizes with clear logging when a dependency is missing. +- [ ] Draw/composition fatalizes with clear logging when a dependency is missing. +- [ ] No runtime path continues scene composition after missing dependency detection. + +## Tests / Validacao + +### Unit Tests + +- Add/update tests for scene decode under the widened `AssetId` field. +- Add/update tests for reverse index maintenance across preload, commit, overwrite, and shutdown. +- Add/update tests for `FrameComposer` bind failure on missing glyph dependency. + +### Integration Tests + +- Add/update runtime integration tests covering: + - scene bind with all dependencies present; + - fatal bind when a dependency is absent; + - fatal draw when a dependency becomes unresolved after bind, if that transition is constructible in tests. + +### Manual Verification + +- Load a cartridge with scene and glyph dependencies committed in non-matching slot order and confirm bind still resolves correctly. +- Confirm fatal logs identify the missing `AssetId` and layer when dependency resolution fails. + +## Riscos + +- Runtime fatalization may expose pre-existing tests or flows that assumed passive failure. +- Reverse-index maintenance can become incorrect if any overwrite/clear path is missed. +- Removing slot ids from scene/cache types can cascade into a larger render refactor than expected if code paths implicitly rely on them. + diff --git a/discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md b/discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md new file mode 100644 index 00000000..8a844e23 --- /dev/null +++ b/discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md @@ -0,0 +1,116 @@ +--- +id: PLN-0042 +ticket: scene-bank-glyph-runtime-binding-leak +title: Scene glyph tooling, fixtures, and test payload migration +status: review +created: 2026-04-24 +completed: +tags: [gfx, runtime, asset, scene, glyph, tooling, tests] +--- + +## Briefing + +Complete the implementation surface of `DEC-0021` by migrating all scene payload producers, fixtures, stress tools, and test data away from serialized runtime glyph slots and onto per-layer glyph dependency `AssetId (i32)`. + +## Decisions de Origem + +- `DEC-0021` — Scene Bank glyph dependencies MUST bind by `asset_id`, not runtime slot. + +## Alvo + +- Runtime tests and helpers under `crates/console/prometeu-drivers/` +- Runtime tests under `crates/console/prometeu-system/` +- Tooling and stress generators under `crates/tools/` + +## Escopo + +- Rewrite scene payload writers in tests and tools. +- Rewrite test fixtures and expectations that still name or assert `glyph_bank_id`. +- Ensure stress and fixture content uses valid `AssetId` dependencies and exercises runtime resolution order independence. + +## Fora de Escopo + +- Core runtime implementation of reverse index and fatal semantics. +- Public API exposure of reverse lookup. +- Backward-compatible scene payload generation. + +## Plano de Execucao + +### Step 1 - Rewrite local scene payload builders in driver tests + +**What:** +Update every driver-side scene helper and binary payload builder that still writes per-layer runtime slot ids. + +**How:** +Search all `SceneLayer` builders and `SCENE` payload encoders in driver tests. Replace slot-oriented setup with per-layer dependency `AssetId`, then adjust assertions accordingly. + +**File(s):** +- `crates/console/prometeu-drivers/src/asset.rs` +- `crates/console/prometeu-drivers/src/frame_composer.rs` +- `crates/console/prometeu-drivers/src/gfx.rs` +- `crates/console/prometeu-drivers/src/hardware.rs` + +### Step 2 - Rewrite system/runtime integration fixtures + +**What:** +Migrate integration fixtures so scene setup reflects asset-id dependencies and runtime slot resolution. + +**How:** +Update helper functions and tests under `prometeu-system` that construct scenes or assume direct slot semantics. Ensure tests explicitly preload/commit glyph assets under varying slot orders and verify correct resolution behavior. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs` +- other runtime test modules that build scene fixtures. + +### Step 3 - Rewrite tooling and stress generators + +**What:** +Update scene-producing tools so published binary payloads match the accepted scene dependency contract. + +**How:** +Rewrite stress/payload builders to write `AssetId (i32)` per layer and to generate coherent asset tables/preload data that prove runtime resolution by dependency identity rather than baked slot topology. + +**File(s):** +- `crates/tools/pbxgen-stress/src/lib.rs` +- any other local scene payload generators discovered during implementation. + +### Step 4 - Add regression coverage for former slot leakage + +**What:** +Add focused regression cases that fail if scene payloads again leak runtime glyph slot ids. + +**How:** +Introduce tests that deliberately load glyph dependencies into different slots from their prior fixture order and verify scene bind/draw behavior depends on `AssetId` resolution only. + +**File(s):** +- affected test modules under `prometeu-drivers`, `prometeu-system`, and tool tests if present. + +## Criterios de Aceite + +- [ ] No test or tooling payload writer serializes per-layer runtime glyph slot id for `SCENE`. +- [ ] Scene fixtures express glyph dependencies by `AssetId`. +- [ ] At least one regression test proves glyph slot order does not matter for scene activation. +- [ ] At least one regression test would fail if old slot-leak behavior returned. + +## Tests / Validacao + +### Unit Tests + +- Update existing scene codec/fixture tests to assert `AssetId`-based dependency behavior. + +### Integration Tests + +- Run runtime integration coverage where scene dependencies resolve correctly despite different glyph slot placement. +- Run regression coverage for fatal behavior when dependencies are absent. + +### Manual Verification + +- Inspect representative generated payload bytes to confirm widened dependency field writing. +- Verify stress/tool outputs remain consumable by the updated runtime. + +## Riscos + +- Test helper churn may be broad because many fixtures were written around the leaked slot model. +- Tool outputs may silently drift if any scene payload writer is missed. +- Regression tests can become brittle if they depend on incidental slot ordering instead of declared dependency identity. + -- 2.47.2 From 26686c288c96db2c3998cead57415b7fa78cd2a1 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 24 Apr 2026 10:57:48 +0100 Subject: [PATCH 2/8] implements PLN-0040 scene glyph spec and wire layout --- crates/console/prometeu-hal/src/asset.rs | 6 +- discussion/index.ndjson | 2 +- ...e-glyph-dependency-spec-and-wire-layout.md | 5 +- docs/specs/runtime/04-gfx-peripheral.md | 14 +++- docs/specs/runtime/15-asset-management.md | 71 +++++++++++++++++-- .../specs/runtime/16-host-abi-and-syscalls.md | 6 ++ 6 files changed, 91 insertions(+), 13 deletions(-) diff --git a/crates/console/prometeu-hal/src/asset.rs b/crates/console/prometeu-hal/src/asset.rs index 36aaf3f9..aa451b0f 100644 --- a/crates/console/prometeu-hal/src/asset.rs +++ b/crates/console/prometeu-hal/src/asset.rs @@ -53,7 +53,11 @@ pub const SCENE_PAYLOAD_MAGIC_V1: [u8; 4] = *b"SCNE"; pub const SCENE_PAYLOAD_VERSION_V1: u16 = 1; pub const SCENE_LAYER_COUNT_V1: usize = 4; pub const SCENE_HEADER_BYTES_V1: usize = 12; -pub const SCENE_LAYER_HEADER_BYTES_V1: usize = 28; +// Per-layer header: +// flags:u8 + glyph_asset_id:i32 + tile_size:u8 + reserved:u16 +// + parallax_x:f32 + parallax_y:f32 + width:u32 + height:u32 +// + tile_count:u32 + reserved:u32 +pub const SCENE_LAYER_HEADER_BYTES_V1: usize = 32; pub const SCENE_TILE_RECORD_BYTES_V1: usize = 4; pub const SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1: usize = 16; diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 17137cf0..91a2e89b 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -21,7 +21,7 @@ {"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-0029","status":"open","ticket":"scene-bank-glyph-runtime-binding-leak","title":"Scene Bank Glyph Runtime Binding Leak","created_at":"2026-04-24","updated_at":"2026-04-24","tags":["gfx","runtime","asset","scene","glyph","format","architecture"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-scene-bank-glyph-runtime-binding-leak.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-24"}],"decisions":[{"id":"DEC-0021","file":"DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md","status":"in_progress","created_at":"2026-04-24","updated_at":"2026-04-24","ref_agenda":"AGD-0029"}],"plans":[{"id":"PLN-0040","file":"PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0041","file":"PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0042","file":"PLN-0042-scene-glyph-tooling-and-fixture-migration.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0029","status":"open","ticket":"scene-bank-glyph-runtime-binding-leak","title":"Scene Bank Glyph Runtime Binding Leak","created_at":"2026-04-24","updated_at":"2026-04-24","tags":["gfx","runtime","asset","scene","glyph","format","architecture"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-scene-bank-glyph-runtime-binding-leak.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-24"}],"decisions":[{"id":"DEC-0021","file":"DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md","status":"in_progress","created_at":"2026-04-24","updated_at":"2026-04-24","ref_agenda":"AGD-0029"}],"plans":[{"id":"PLN-0040","file":"PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0041","file":"PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0042","file":"PLN-0042-scene-glyph-tooling-and-fixture-migration.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]}],"lessons":[]} {"type":"discussion","id":"DSC-0014","status":"done","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0035","file":"lessons/DSC-0014-perf-vm-allocation-and-copy-pressure/LSN-0035-first-materialization-is-not-the-same-as-hot-path-copy-pressure.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]} {"type":"discussion","id":"DSC-0015","status":"abandoned","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"abandoned","created_at":"2026-03-27","updated_at":"2026-04-20","_override_reason":"User explicitly chose to close the discussion without decision because FS->memory copy for the program is already acceptable."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to abandon the discussion without creating a decision because FS->memory copy for the program is already acceptable."} {"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/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md b/discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md index 0835ac0f..34e13a0e 100644 --- a/discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md +++ b/discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md @@ -2,9 +2,9 @@ id: PLN-0040 ticket: scene-bank-glyph-runtime-binding-leak title: Scene glyph dependency spec and wire layout migration -status: review +status: done created: 2026-04-24 -completed: +completed: 2026-04-24 tags: [gfx, runtime, asset, scene, glyph, format, spec] --- @@ -102,4 +102,3 @@ Remove proposed/legacy wording that frames missing dependencies as recoverable s - Spec-only edits can drift from the runtime implementation if later plans are not executed promptly. - Scene payload layout changes may require touching multiple docs; missing one will leave contradictory contract text. - If HAL constants are updated here before runtime code, temporary compile or test fallout may appear in later plans and must be handled intentionally. - diff --git a/docs/specs/runtime/04-gfx-peripheral.md b/docs/specs/runtime/04-gfx-peripheral.md index 8b63b63a..9948ec54 100644 --- a/docs/specs/runtime/04-gfx-peripheral.md +++ b/docs/specs/runtime/04-gfx-peripheral.md @@ -579,7 +579,7 @@ Fault boundary: | `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.bind_scene` | `status:int` | status-returning API, but missing scene glyph dependencies are fatal runtime errors | | `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 | @@ -596,6 +596,18 @@ The public `gfx.*` primitive family remains valid in v1, but its stable operatio 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.1.b Scene dependency fatal boundary + +`composer.bind_scene` remains a status-returning syscall in the public ABI, but scene glyph dependency absence is outside the accepted passive operational-error model. + +Rules: + +- the target scene slot may still be rejected through ordinary status-returning behavior when the scene itself is unavailable; +- missing glyph dependencies referenced by a resident scene are not a passive `status:int` case; +- if scene activation discovers that a layer dependency cannot be resolved to a committed glyph asset, the machine MUST fail fatally and emit a clear log; +- if scene composition later discovers that a layer dependency can no longer be resolved, the machine MUST fail fatally and emit a clear log; +- runtime MUST NOT continue canonical scene composition after such a dependency failure. + ### 19.2 `composer.emit_sprite` `composer.emit_sprite` returns `status:int`. diff --git a/docs/specs/runtime/15-asset-management.md b/docs/specs/runtime/15-asset-management.md index 9de63331..cf76e3c3 100644 --- a/docs/specs/runtime/15-asset-management.md +++ b/docs/specs/runtime/15-asset-management.md @@ -108,9 +108,9 @@ This table describes content identity and storage layout, not live residency. `codec` does not define the bank-specific serialized layout itself. Specialized banks may still have normative decode rules even when `codec = NONE`. -### 4.1 `TILES` asset contract in v1 +### 4.1 `GLYPH` asset contract in v1 -For `BankType::TILES`, the v1 runtime-facing contract is: +For `BankType::GLYPH`, the v1 runtime-facing contract is: - `codec = NONE` - serialized pixels use packed `u4` palette indices @@ -118,20 +118,20 @@ For `BankType::TILES`, the v1 runtime-facing contract is: - `palette_count = 64` - runtime materialization may expand pixel indices to one `u8` per pixel -For `TILES`, `NONE` means there is no additional generic codec layer beyond the bank contract itself. +For `GLYPH`, `NONE` means there is no additional generic codec layer beyond the bank contract itself. For the current transition window: - `RAW` is a legacy deprecated alias of `NONE` - newly published material must use `NONE` as the canonical value -Even with `codec = NONE`, `TILES` still requires bank-specific decode from its serialized payload. The serialized byte layout therefore does not need to match the in-memory layout. +Even with `codec = NONE`, `GLYPH` still requires bank-specific decode from its serialized payload. The serialized byte layout therefore does not need to match the in-memory layout. #### 4.1.1 Metadata Normalization Following `DEC-0004`, the `AssetEntry.metadata` field must be structured in segmented form to avoid ambiguity. -Required effective metadata fields for `TILES` at the root level: +Required effective metadata fields for `GLYPH` at the root level: - `tile_size`: tile edge in pixels; valid values are `8`, `16`, or `32` - `width`: total sheet width in pixels @@ -143,7 +143,7 @@ Optional informative subtrees: - `metadata.codec`: codec/compressor-specific configuration (for example dictionaries or compression flags). - `metadata.pipeline`: informative metadata from the Studio build process (for example source hashes, timestamps, or tool versions). -Validation rules for `TILES` v1: +Validation rules for `GLYPH` v1: - `palette_count` must be `64` - `width * height` defines the number of logical indexed pixels in the decoded sheet @@ -160,17 +160,74 @@ The tile-bank payload therefore separates serialized storage form from runtime m - decoded pixel plane: expanded `u8` indices, one entry per pixel - palette table: `64 * 16` colors in `RGB565` -For `TILES` v1: +For `GLYPH` v1: - `size` must match `ceil(width * height / 2) + (palette_count * 16 * 2)` - `decoded_size` must match `(width * height) + (palette_count * 16 * 2)` +### 4.2 `SCENE` asset contract in v1 + +For `BankType::SCENE`, the v1 runtime-facing contract is: + +- `codec = NONE` +- payload magic is `SCNE` +- payload version is `1` +- layer count is fixed at `4` +- each layer carries its own glyph dependency +- the glyph dependency is serialized as canonical `AssetId` using signed 32-bit little-endian width +- the payload MUST describe cold asset dependencies only and MUST NOT encode runtime glyph slot topology + +Even with `codec = NONE`, `SCENE` still has a bank-specific binary contract. The serialized layout is normative and distinct from any transient runtime residency state. + +#### 4.2.1 Layer Header Layout + +Each scene layer header is: + +1. `flags: u8` +2. `glyph_asset_id: i32` (little-endian) +3. `tile_size: u8` +4. `reserved: u16` +5. `parallax_x: f32` +6. `parallax_y: f32` +7. `width: u32` +8. `height: u32` +9. `tile_count: u32` +10. `reserved: u32` + +This makes the canonical per-layer header size `32` bytes. + +Normative rules: + +- `glyph_asset_id` MUST be interpreted as the cold identity of the glyph dependency for that layer; +- `glyph_asset_id` MUST NOT be interpreted as a runtime slot id; +- each layer MUST own exactly one glyph dependency reference; +- no shared dependency table exists in this contract; +- `tile_count` MUST equal `width * height`. + +#### 4.2.2 Runtime Binding Semantics + +`SCENE` assets do not own runtime residency. + +Therefore: + +- the runtime MUST resolve scene glyph dependencies through runtime-owned residency state; +- the runtime MUST maintain a runtime-owned mapping from committed glyph `AssetId` to glyph slot; +- scene bind and scene draw MUST consult runtime residency, not serialized slot topology; +- a missing scene glyph dependency is not a passive asset-status condition. + +If a scene glyph dependency cannot be resolved: + +- `bind_scene` MUST fail as a fatal machine error with clear logging; +- draw/composition MUST fail as a fatal machine error with clear logging; +- runtime MUST NOT continue scene composition after the missing dependency is detected. + ## 5 Banks and Slots The current runtime exposes bank types: - `GLYPH` - `SOUNDS` +- `SCENE` Assets are loaded into explicit slots identified by slot index at the public ABI boundary. diff --git a/docs/specs/runtime/16-host-abi-and-syscalls.md b/docs/specs/runtime/16-host-abi-and-syscalls.md index 77d66115..89595065 100644 --- a/docs/specs/runtime/16-host-abi-and-syscalls.md +++ b/docs/specs/runtime/16-host-abi-and-syscalls.md @@ -229,6 +229,12 @@ For mutating composer operations: - `set_camera` remains `void` in v1; - no caller-provided sprite index or `active` flag is part of the canonical contract. +Fatal boundary clarification for `bind_scene`: + +- `bind_scene` remains status-returning for ordinary orchestration-domain rejection such as unresolved target scene slot; +- missing glyph dependencies inside a resident scene are not accepted as passive `ComposerOpStatus` outcomes; +- if scene activation or later composition detects an unresolved scene glyph dependency, runtime MUST escalate that condition as a fatal machine error with clear logging. + ## 7 Syscalls as Callable Entities (Not First-Class) Syscalls behave like call sites, not like first-class guest values. -- 2.47.2 From 0d6ff430a055912d53b412524f3be93071d415df Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 24 Apr 2026 11:08:39 +0100 Subject: [PATCH 3/8] implements PLN-0041 scene glyph runtime resolution --- crates/console/prometeu-drivers/src/asset.rs | 127 +++++++++----- .../prometeu-drivers/src/frame_composer.rs | 165 ++++++++++++++++-- crates/console/prometeu-drivers/src/gfx.rs | 56 ++++-- .../console/prometeu-drivers/src/hardware.rs | 21 ++- crates/console/prometeu-hal/src/gfx_bridge.rs | 7 +- crates/console/prometeu-hal/src/scene_bank.rs | 7 +- .../console/prometeu-hal/src/scene_layer.rs | 7 +- .../prometeu-hal/src/scene_viewport_cache.rs | 20 ++- .../src/scene_viewport_resolver.rs | 3 +- .../src/virtual_machine_runtime/dispatch.rs | 16 +- .../src/virtual_machine_runtime/tests.rs | 16 +- .../src/virtual_machine_runtime/tick.rs | 25 ++- discussion/index.ndjson | 2 +- ...lyph-runtime-resolution-and-fatal-paths.md | 5 +- 14 files changed, 368 insertions(+), 109 deletions(-) diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index 99d33cf1..2b1424d2 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -32,6 +32,35 @@ type StagingMap = HashMap>; type AssetTable = HashMap; type HandleTable = HashMap; +#[derive(Clone, Default)] +pub struct GlyphAssetSlotIndex { + slots_by_asset_id: Arc>>, +} + +impl GlyphAssetSlotIndex { + pub fn new() -> Self { + Self { slots_by_asset_id: Arc::new(RwLock::new(HashMap::new())) } + } + + pub fn glyph_slot_for_asset(&self, asset_id: AssetId) -> Option { + self.slots_by_asset_id.read().unwrap().get(&asset_id).copied() + } + + pub fn rebuild_from_slots(&self, slots: &[Option; 16]) { + let mut map = self.slots_by_asset_id.write().unwrap(); + map.clear(); + for (slot, asset_id) in slots.iter().enumerate() { + if let Some(asset_id) = asset_id { + map.insert(*asset_id, slot); + } + } + } + + fn clear(&self) { + self.slots_by_asset_id.write().unwrap().clear(); + } +} + const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64; const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16; const GLYPH_BANK_PALETTE_BYTES_V1: usize = @@ -163,6 +192,7 @@ pub struct AssetManager { /// Track what is installed in each hardware slot (for stats/info). gfx_slots: Arc; 16]>>, + glyph_slot_index: GlyphAssetSlotIndex, sound_slots: Arc; 16]>>, scene_slots: Arc; 16]>>, @@ -309,6 +339,7 @@ impl AssetManager { sound_installer, scene_installer, gfx_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))), + glyph_slot_index: GlyphAssetSlotIndex::new(), sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))), scene_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))), gfx_policy: BankPolicy::new(), @@ -361,6 +392,7 @@ impl AssetManager { let mut slots = self.gfx_slots.write().unwrap(); if slot_index < slots.len() { slots[slot_index] = Some(entry.asset_id); + self.glyph_slot_index.rebuild_from_slots(&slots); } } } @@ -746,7 +778,7 @@ impl AssetManager { let mut decoded_size = 0_usize; let layers = std::array::from_fn(|_| SceneLayer { active: false, - glyph_bank_id: 0, + glyph_asset_id: 0, tile_size: TileSize::Size8, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width: 0, height: 0, tiles: Vec::new() }, @@ -762,8 +794,13 @@ impl AssetManager { } let flags = buffer[offset]; - let glyph_bank_id = buffer[offset + 1]; - let tile_size_raw = buffer[offset + 2]; + let glyph_asset_id = i32::from_le_bytes([ + buffer[offset + 1], + buffer[offset + 2], + buffer[offset + 3], + buffer[offset + 4], + ]); + let tile_size_raw = buffer[offset + 5]; let tile_size = match tile_size_raw { 8 => TileSize::Size8, 16 => TileSize::Size16, @@ -771,39 +808,39 @@ impl AssetManager { other => return Err(format!("Invalid SCENE tile size: {}", other)), }; let parallax_factor_x = f32::from_le_bytes([ - buffer[offset + 4], - buffer[offset + 5], - buffer[offset + 6], - buffer[offset + 7], - ]); - let parallax_factor_y = f32::from_le_bytes([ buffer[offset + 8], buffer[offset + 9], buffer[offset + 10], buffer[offset + 11], ]); + let parallax_factor_y = f32::from_le_bytes([ + buffer[offset + 12], + buffer[offset + 13], + buffer[offset + 14], + buffer[offset + 15], + ]); 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([ - buffer[offset + 12], - buffer[offset + 13], - buffer[offset + 14], - buffer[offset + 15], - ]) as usize; - let height = u32::from_le_bytes([ buffer[offset + 16], buffer[offset + 17], buffer[offset + 18], buffer[offset + 19], ]) as usize; - let tile_count = u32::from_le_bytes([ + let height = u32::from_le_bytes([ buffer[offset + 20], buffer[offset + 21], buffer[offset + 22], buffer[offset + 23], ]) as usize; + let tile_count = u32::from_le_bytes([ + buffer[offset + 24], + buffer[offset + 25], + buffer[offset + 26], + buffer[offset + 27], + ]) as usize; let expected_tile_count = width.checked_mul(height).ok_or("SCENE tile count overflow")?; @@ -845,7 +882,7 @@ impl AssetManager { *layer = SceneLayer { active: (flags & 0b0000_0001) != 0, - glyph_bank_id, + glyph_asset_id, tile_size, parallax_factor: ParallaxFactor { x: parallax_factor_x, y: parallax_factor_y }, tilemap: TileMap { width, height, tiles }, @@ -960,6 +997,7 @@ impl AssetManager { let mut slots = self.gfx_slots.write().unwrap(); if h.slot.index < slots.len() { slots[h.slot.index] = Some(h._asset_id); + self.glyph_slot_index.rebuild_from_slots(&slots); } h.status = LoadStatus::COMMITTED; } @@ -1089,9 +1127,14 @@ impl AssetManager { self.handles.write().unwrap().clear(); self.pending_commits.lock().unwrap().clear(); self.gfx_slots.write().unwrap().fill(None); + self.glyph_slot_index.clear(); self.sound_slots.write().unwrap().fill(None); self.scene_slots.write().unwrap().fill(None); } + + pub fn glyph_asset_slot_index(&self) -> GlyphAssetSlotIndex { + self.glyph_slot_index.clone() + } } #[cfg(test)] @@ -1144,9 +1187,13 @@ mod tests { fn test_scene() -> SceneBank { let make_layer = - |glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, tile_size: TileSize| SceneLayer { - active: glyph_bank_id != 3, - glyph_bank_id, + |glyph_asset_id: AssetId, + active: bool, + parallax_x: f32, + parallax_y: f32, + tile_size: TileSize| SceneLayer { + active, + glyph_asset_id, tile_size, parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y }, tilemap: TileMap { @@ -1155,37 +1202,25 @@ mod tests { tiles: vec![ Tile { active: true, - glyph: Glyph { - glyph_id: 10 + glyph_bank_id as u16, - palette_id: glyph_bank_id, - }, + glyph: Glyph { glyph_id: 10, palette_id: 1 }, flip_x: false, flip_y: false, }, Tile { active: true, - glyph: Glyph { - glyph_id: 20 + glyph_bank_id as u16, - palette_id: glyph_bank_id + 1, - }, + glyph: Glyph { glyph_id: 20, palette_id: 2 }, flip_x: true, flip_y: false, }, Tile { active: true, - glyph: Glyph { - glyph_id: 30 + glyph_bank_id as u16, - palette_id: glyph_bank_id + 2, - }, + glyph: Glyph { glyph_id: 30, palette_id: 3 }, flip_x: false, flip_y: true, }, Tile { - active: glyph_bank_id != 2, - glyph: Glyph { - glyph_id: 40 + glyph_bank_id as u16, - palette_id: glyph_bank_id + 3, - }, + active, + glyph: Glyph { glyph_id: 40, palette_id: 4 }, flip_x: true, flip_y: true, }, @@ -1195,10 +1230,10 @@ mod tests { SceneBank { layers: [ - make_layer(0, 1.0, 1.0, TileSize::Size16), - make_layer(1, 0.5, 0.75, TileSize::Size8), - make_layer(2, 1.0, 0.5, TileSize::Size32), - make_layer(3, 0.25, 0.25, TileSize::Size16), + make_layer(100, true, 1.0, 1.0, TileSize::Size16), + make_layer(101, true, 0.5, 0.75, TileSize::Size8), + make_layer(102, true, 1.0, 0.5, TileSize::Size32), + make_layer(103, false, 0.25, 0.25, TileSize::Size16), ], } } @@ -1224,9 +1259,9 @@ mod tests { 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.extend_from_slice(&layer.glyph_asset_id.to_le_bytes()); data.push(layer.tile_size as u8); - data.push(0); + data.extend_from_slice(&0_u16.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()); @@ -1358,7 +1393,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].glyph_asset_id, 101); 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); @@ -1382,7 +1417,7 @@ mod tests { fn test_decode_scene_bank_rejects_invalid_tile_size() { let scene = test_scene(); let mut data = encode_scene_payload(&scene); - data[14] = 12; + data[17] = 12; let entry = test_scene_asset_entry("scene", &data, &scene); let err = AssetManager::decode_scene_bank_from_buffer(&entry, &data).unwrap_err(); @@ -1406,7 +1441,7 @@ mod tests { fn test_decode_scene_bank_rejects_tile_count_mismatch() { let scene = test_scene(); let mut data = encode_scene_payload(&scene); - data[32..36].copy_from_slice(&5_u32.to_le_bytes()); + data[36..40].copy_from_slice(&5_u32.to_le_bytes()); let entry = test_scene_asset_entry("scene", &data, &scene); let err = AssetManager::decode_scene_bank_from_buffer(&entry, &data).unwrap_err(); diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index 02a2f471..faa1a00c 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -1,3 +1,4 @@ +use crate::asset::GlyphAssetSlotIndex; use crate::memory_banks::SceneBankPoolAccess; use prometeu_hal::GfxBridge; use prometeu_hal::glyph::Glyph; @@ -116,6 +117,7 @@ impl SpriteController { pub struct FrameComposer { scene_bank_pool: Arc, + glyph_slot_index: GlyphAssetSlotIndex, viewport_width_px: usize, viewport_height_px: usize, active_scene_id: Option, @@ -133,9 +135,11 @@ impl FrameComposer { viewport_width_px: usize, viewport_height_px: usize, scene_bank_pool: Arc, + glyph_slot_index: GlyphAssetSlotIndex, ) -> Self { Self { scene_bank_pool, + glyph_slot_index, viewport_width_px, viewport_height_px, active_scene_id: None, @@ -187,6 +191,8 @@ impl FrameComposer { return false; }; + let _ = self.resolve_glyph_slots(&scene, scene_bank_id, "bind_scene"); + let (cache, resolver) = Self::build_scene_runtime(self.viewport_width_px, self.viewport_height_px, &scene); self.active_scene_id = Some(scene_bank_id); @@ -242,15 +248,19 @@ impl FrameComposer { 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()) - { + if let Some(scene) = self.active_scene.as_deref() { + let scene_bank_id = self.active_scene_id.expect("active scene id must exist"); + let resolved_glyph_slots = + self.resolve_glyph_slots(scene, scene_bank_id, "render_frame"); + let (Some(cache), Some(resolver)) = (self.cache.as_mut(), self.resolver.as_mut()) else { + panic!("SCENE runtime invariant broken: active scene without cache/resolver"); + }; 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); + gfx.render_scene_from_cache(cache, &update, &resolved_glyph_slots); return; } @@ -284,6 +294,23 @@ impl FrameComposer { ) } + fn resolve_glyph_slots( + &self, + scene: &SceneBank, + scene_bank_id: usize, + phase: &str, + ) -> [usize; 4] { + std::array::from_fn(|layer_index| { + let layer = &scene.layers[layer_index]; + self.glyph_slot_index.glyph_slot_for_asset(layer.glyph_asset_id).unwrap_or_else(|| { + panic!( + "SCENE dependency fatal during {}: scene_bank_id={}, layer={}, glyph_asset_id={}", + phase, scene_bank_id, layer_index, layer.glyph_asset_id + ) + }) + }) + } + fn apply_refresh_requests( cache: &mut SceneViewportCache, scene: &SceneBank, @@ -316,23 +343,25 @@ mod tests { GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller, }; use prometeu_hal::color::Color; + use prometeu_hal::asset::AssetId; use prometeu_hal::glyph_bank::{GlyphBank, TileSize}; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; + use std::panic::{AssertUnwindSafe, catch_unwind}; fn make_scene() -> SceneBank { make_scene_with_palette(1, 1, TileSize::Size8) } fn make_scene_with_palette( - glyph_bank_id: u8, + glyph_asset_id: AssetId, palette_id: u8, tile_size: TileSize, ) -> SceneBank { let layer = SceneLayer { active: true, - glyph_bank_id, + glyph_asset_id, tile_size, parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 }, tilemap: TileMap { @@ -353,6 +382,16 @@ mod tests { SceneBank { layers: std::array::from_fn(|_| layer.clone()) } } + fn make_glyph_slot_index(bindings: &[(AssetId, usize)]) -> GlyphAssetSlotIndex { + let index = GlyphAssetSlotIndex::new(); + let mut slots = [None; 16]; + for (asset_id, slot) in bindings { + slots[*slot] = Some(*asset_id); + } + index.rebuild_from_slots(&slots); + index + } + 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); @@ -365,7 +404,12 @@ mod tests { #[test] fn frame_composer_starts_unbound_with_empty_owned_state() { - let frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + let frame_composer = FrameComposer::new( + 320, + 180, + Arc::new(MemoryBanks::new()), + make_glyph_slot_index(&[]), + ); assert_eq!(frame_composer.viewport_size(), (320, 180)); assert_eq!(frame_composer.active_scene_id(), None); @@ -384,7 +428,7 @@ mod tests { let banks = Arc::new(MemoryBanks::new()); banks.install_scene_bank(3, Arc::new(make_scene())); - let frame_composer = FrameComposer::new(320, 180, banks); + let frame_composer = FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[(1, 0)])); let scene = frame_composer.scene_bank_slot(3).expect("scene bank slot 3 should be resident"); @@ -400,7 +444,8 @@ mod tests { 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); + let mut frame_composer = + FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[(1, 0)])); assert!(frame_composer.bind_scene(3)); @@ -419,7 +464,8 @@ mod tests { let banks = Arc::new(MemoryBanks::new()); banks.install_scene_bank(1, Arc::new(make_scene())); - let mut frame_composer = FrameComposer::new(320, 180, banks); + let mut frame_composer = + FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[(1, 0)])); assert!(frame_composer.bind_scene(1)); frame_composer.unbind_scene(); @@ -433,7 +479,12 @@ mod tests { #[test] fn set_camera_stores_top_left_pixel_coordinates() { - let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + let mut frame_composer = FrameComposer::new( + 320, + 180, + Arc::new(MemoryBanks::new()), + make_glyph_slot_index(&[]), + ); frame_composer.set_camera(-12, 48); @@ -445,7 +496,8 @@ mod tests { let banks = Arc::new(MemoryBanks::new()); banks.install_scene_bank(0, Arc::new(make_scene())); - let mut frame_composer = FrameComposer::new(320, 180, banks); + let mut frame_composer = + FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[(1, 0)])); assert!(frame_composer.bind_scene(0)); let cache = frame_composer.cache().expect("cache should exist for bound scene"); @@ -454,7 +506,12 @@ mod tests { #[test] fn missing_scene_binding_falls_back_to_no_scene_state() { - let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + let mut frame_composer = FrameComposer::new( + 320, + 180, + Arc::new(MemoryBanks::new()), + make_glyph_slot_index(&[]), + ); assert!(!frame_composer.bind_scene(7)); @@ -463,6 +520,28 @@ mod tests { assert!(frame_composer.resolver().is_none()); } + #[test] + fn bind_scene_panics_when_scene_dependency_is_not_resident() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_scene_bank(0, Arc::new(make_scene())); + + let mut frame_composer = + FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[])); + + let panic = catch_unwind(AssertUnwindSafe(|| frame_composer.bind_scene(0))) + .expect_err("bind_scene should panic when dependency is missing"); + let message = if let Some(message) = panic.downcast_ref::() { + message.clone() + } else if let Some(message) = panic.downcast_ref::<&str>() { + (*message).to_string() + } else { + String::new() + }; + + assert!(message.contains("SCENE dependency fatal during bind_scene")); + assert!(message.contains("glyph_asset_id=1")); + } + #[test] fn sprite_controller_begin_frame_resets_sprite_count_and_buckets() { let mut controller = SpriteController::new(); @@ -582,7 +661,12 @@ mod tests { #[test] fn frame_composer_emits_ordered_sprites_for_rendering() { - let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + let mut frame_composer = FrameComposer::new( + 320, + 180, + Arc::new(MemoryBanks::new()), + make_glyph_slot_index(&[]), + ); frame_composer.begin_frame(); assert!(frame_composer.emit_sprite(Sprite { @@ -621,7 +705,12 @@ mod tests { 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); + FrameComposer::new( + 16, + 16, + Arc::clone(&banks) as Arc, + make_glyph_slot_index(&[]), + ); frame_composer.begin_frame(); assert!(frame_composer.emit_sprite(Sprite { glyph: Glyph { glyph_id: 0, palette_id: 3 }, @@ -652,7 +741,12 @@ mod tests { 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); + FrameComposer::new( + 16, + 16, + Arc::clone(&banks) as Arc, + make_glyph_slot_index(&[(0, 0)]), + ); assert!(frame_composer.bind_scene(0)); let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); @@ -683,7 +777,12 @@ mod tests { 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); + FrameComposer::new( + 16, + 16, + Arc::clone(&banks) as Arc, + make_glyph_slot_index(&[(0, 0), (1, 1)]), + ); let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); gfx.scene_fade_level = 31; gfx.hud_fade_level = 31; @@ -716,4 +815,36 @@ mod tests { gfx.present(); assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw()); } + + #[test] + fn render_frame_panics_when_dependency_disappears_after_bind() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 1, Color::RED))); + banks.install_scene_bank(0, Arc::new(make_scene_with_palette(1, 1, TileSize::Size8))); + + let glyph_slot_index = make_glyph_slot_index(&[(1, 0)]); + let mut frame_composer = FrameComposer::new( + 16, + 16, + Arc::clone(&banks) as Arc, + glyph_slot_index.clone(), + ); + assert!(frame_composer.bind_scene(0)); + + glyph_slot_index.rebuild_from_slots(&[None; 16]); + + let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); + let panic = catch_unwind(AssertUnwindSafe(|| frame_composer.render_frame(&mut gfx))) + .expect_err("render_frame should panic when dependency disappears"); + let message = if let Some(message) = panic.downcast_ref::() { + message.clone() + } else if let Some(message) = panic.downcast_ref::<&str>() { + (*message).to_string() + } else { + String::new() + }; + + assert!(message.contains("SCENE dependency fatal during render_frame")); + assert!(message.contains("glyph_asset_id=1")); + } } diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index a39e0658..cbf4579d 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -222,8 +222,13 @@ impl GfxBridge for Gfx { 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) + fn render_scene_from_cache( + &mut self, + cache: &SceneViewportCache, + update: &ResolverUpdate, + resolved_glyph_slots: &[usize; 4], + ) { + self.render_scene_from_cache(cache, update, resolved_glyph_slots) } fn load_frame_sprites(&mut self, sprites: &[Sprite]) { self.load_frame_sprites(sprites) @@ -646,7 +651,12 @@ impl Gfx { /// This is the cache-backed world path accepted by DEC-0013. The canonical scene /// is not consulted here; the renderer only consumes prepared cache materialization /// plus sprite state and fade controls. - pub fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) { + pub fn render_scene_from_cache( + &mut self, + cache: &SceneViewportCache, + update: &ResolverUpdate, + resolved_glyph_slots: &[usize; 4], + ) { self.back.fill(Color::BLACK.raw()); self.populate_layer_buckets(); @@ -665,6 +675,7 @@ impl Gfx { self.h, cache, &update.copy_requests[layer_index], + resolved_glyph_slots[layer_index], &*self.glyph_banks, ); } @@ -695,6 +706,7 @@ impl Gfx { screen_h: usize, cache: &SceneViewportCache, request: &LayerCopyRequest, + glyph_slot: usize, glyph_banks: &dyn GlyphBankPoolAccess, ) { let mut target = RenderTarget { back, screen_w, screen_h }; @@ -703,8 +715,11 @@ impl Gfx { return; } - let Some(bank) = glyph_banks.glyph_bank_slot(layer_cache.glyph_bank_id as usize) else { - return; + let Some(bank) = glyph_banks.glyph_bank_slot(glyph_slot) else { + panic!( + "SCENE composition fatal: resolved glyph slot {} for layer {} is not resident", + glyph_slot, request.layer_index + ); }; let tile_size_px = request.tile_size as i32; @@ -894,8 +909,10 @@ impl Gfx { #[cfg(test)] mod tests { use super::*; + use crate::asset::GlyphAssetSlotIndex; use crate::FrameComposer; use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess}; + use prometeu_hal::asset::AssetId; use prometeu_hal::glyph_bank::TileSize; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; @@ -919,7 +936,7 @@ mod tests { } fn make_layer( - glyph_bank_id: u8, + glyph_asset_id: AssetId, glyph_id: u16, palette_id: u8, width: usize, @@ -927,7 +944,7 @@ mod tests { ) -> SceneLayer { SceneLayer { active: true, - glyph_bank_id, + glyph_asset_id, tile_size: TileSize::Size8, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { @@ -946,10 +963,10 @@ mod tests { } } - fn make_inactive_layer(glyph_bank_id: u8, width: usize, height: usize) -> SceneLayer { + fn make_inactive_layer(glyph_asset_id: AssetId, width: usize, height: usize) -> SceneLayer { SceneLayer { active: false, - glyph_bank_id, + glyph_asset_id, tile_size: TileSize::Size8, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] }, @@ -978,6 +995,16 @@ mod tests { } } + fn make_glyph_slot_index(bindings: &[(AssetId, usize)]) -> GlyphAssetSlotIndex { + let index = GlyphAssetSlotIndex::new(); + let mut slots = [None; 16]; + for (asset_id, slot) in bindings { + slots[*slot] = Some(*asset_id); + } + index.rebuild_from_slots(&slots); + index + } + #[test] fn test_draw_pixel() { let banks = Arc::new(MemoryBanks::new()); @@ -1054,7 +1081,12 @@ mod tests { 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); + FrameComposer::new( + 32, + 18, + Arc::clone(&banks) as Arc, + make_glyph_slot_index(&[]), + ); gfx.begin_overlay_frame(); frame_composer.begin_frame(); @@ -1095,7 +1127,7 @@ mod tests { let mut gfx = Gfx::new(16, 16, banks); gfx.scene_fade_level = 31; gfx.hud_fade_level = 31; - gfx.render_scene_from_cache(&cache, &update); + gfx.render_scene_from_cache(&cache, &update, &[0, 0, 0, 0]); assert_eq!(gfx.back[0], Color::RED.raw()); } @@ -1146,7 +1178,7 @@ mod tests { }; gfx.sprite_count = 2; - gfx.render_scene_from_cache(&cache, &update); + gfx.render_scene_from_cache(&cache, &update, &[0, 0, 0, 0]); assert_eq!(gfx.back[0], Color::BLUE.raw()); } diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index a7f09aec..4a6138f4 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -126,6 +126,14 @@ impl Hardware { /// Creates hardware with explicit shared bank ownership. pub fn new_with_memory_banks(memory_banks: Arc) -> Self { + let assets = AssetManager::new( + vec![], + AssetsPayloadSource::empty(), + Arc::clone(&memory_banks) as Arc, + Arc::clone(&memory_banks) as Arc, + Arc::clone(&memory_banks) as Arc, + ); + let glyph_slot_index = assets.glyph_asset_slot_index(); Self { gfx: Gfx::new( Self::W, @@ -136,17 +144,12 @@ impl Hardware { Self::W, Self::H, Arc::clone(&memory_banks) as Arc, + glyph_slot_index, ), audio: Audio::new(Arc::clone(&memory_banks) as Arc), pad: Pad::default(), touch: Touch::default(), - assets: AssetManager::new( - vec![], - AssetsPayloadSource::empty(), - Arc::clone(&memory_banks) as Arc, - Arc::clone(&memory_banks) as Arc, - Arc::clone(&memory_banks) as Arc, - ), + assets, } } } @@ -179,7 +182,7 @@ mod tests { fn make_scene() -> SceneBank { let layer = SceneLayer { active: true, - glyph_bank_id: 0, + glyph_asset_id: 1, tile_size: TileSize::Size8, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { @@ -216,7 +219,7 @@ mod tests { hardware.gfx.scene_fade_level = 31; hardware.gfx.hud_fade_level = 31; - hardware.gfx.render_scene_from_cache(&cache, &update); + hardware.gfx.render_scene_from_cache(&cache, &update, &[0, 0, 0, 0]); hardware.gfx.present(); assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw()); diff --git a/crates/console/prometeu-hal/src/gfx_bridge.rs b/crates/console/prometeu-hal/src/gfx_bridge.rs index dc989f82..34dd1465 100644 --- a/crates/console/prometeu-hal/src/gfx_bridge.rs +++ b/crates/console/prometeu-hal/src/gfx_bridge.rs @@ -57,7 +57,12 @@ pub trait GfxBridge { /// /// 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 render_scene_from_cache( + &mut self, + cache: &SceneViewportCache, + update: &ResolverUpdate, + resolved_glyph_slots: &[usize; 4], + ); fn load_frame_sprites(&mut self, sprites: &[Sprite]); /// Submit text into the `gfx.*` primitive path. /// diff --git a/crates/console/prometeu-hal/src/scene_bank.rs b/crates/console/prometeu-hal/src/scene_bank.rs index 6abb47d5..aa4d0d3a 100644 --- a/crates/console/prometeu-hal/src/scene_bank.rs +++ b/crates/console/prometeu-hal/src/scene_bank.rs @@ -8,16 +8,17 @@ pub struct SceneBank { #[cfg(test)] mod tests { use super::*; + use crate::asset::AssetId; use crate::glyph::Glyph; use crate::glyph_bank::TileSize; use crate::scene_layer::ParallaxFactor; use crate::tile::Tile; use crate::tilemap::TileMap; - fn layer(glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer { + fn layer(glyph_asset_id: AssetId, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer { SceneLayer { active: true, - glyph_bank_id, + glyph_asset_id, tile_size: TileSize::Size16, parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y }, tilemap: TileMap { @@ -25,7 +26,7 @@ mod tests { height: 1, tiles: vec![Tile { active: true, - glyph: Glyph { glyph_id, palette_id: glyph_bank_id }, + glyph: Glyph { glyph_id, palette_id: 1 }, flip_x: false, flip_y: false, }], diff --git a/crates/console/prometeu-hal/src/scene_layer.rs b/crates/console/prometeu-hal/src/scene_layer.rs index 90feec93..21427d22 100644 --- a/crates/console/prometeu-hal/src/scene_layer.rs +++ b/crates/console/prometeu-hal/src/scene_layer.rs @@ -1,3 +1,4 @@ +use crate::asset::AssetId; use crate::glyph_bank::TileSize; use crate::tilemap::TileMap; @@ -10,7 +11,7 @@ pub struct ParallaxFactor { #[derive(Clone, Debug)] pub struct SceneLayer { pub active: bool, - pub glyph_bank_id: u8, + pub glyph_asset_id: AssetId, pub tile_size: TileSize, pub parallax_factor: ParallaxFactor, pub tilemap: TileMap, @@ -26,7 +27,7 @@ mod tests { fn scene_layer_preserves_parallax_factor_and_tilemap_ownership() { let layer = SceneLayer { active: true, - glyph_bank_id: 7, + glyph_asset_id: 7, tile_size: TileSize::Size16, parallax_factor: ParallaxFactor { x: 0.5, y: 0.75 }, tilemap: TileMap { @@ -49,7 +50,7 @@ mod tests { }, }; - assert_eq!(layer.glyph_bank_id, 7); + assert_eq!(layer.glyph_asset_id, 7); assert_eq!(layer.parallax_factor.x, 0.5); assert_eq!(layer.parallax_factor.y, 0.75); assert_eq!(layer.tilemap.width, 2); diff --git a/crates/console/prometeu-hal/src/scene_viewport_cache.rs b/crates/console/prometeu-hal/src/scene_viewport_cache.rs index 028cc7ff..8a7bf8ce 100644 --- a/crates/console/prometeu-hal/src/scene_viewport_cache.rs +++ b/crates/console/prometeu-hal/src/scene_viewport_cache.rs @@ -1,3 +1,4 @@ +use crate::asset::AssetId; use crate::glyph_bank::TileSize; use crate::scene_bank::SceneBank; use crate::scene_layer::SceneLayer; @@ -12,7 +13,7 @@ pub struct CachedTileEntry { pub glyph_id: u16, pub palette_id: u8, pub flags: u8, - pub glyph_bank_id: u8, + pub glyph_asset_id: AssetId, } impl CachedTileEntry { @@ -38,7 +39,7 @@ impl CachedTileEntry { glyph_id: tile.glyph.glyph_id, palette_id: tile.glyph.palette_id, flags, - glyph_bank_id: layer.glyph_bank_id, + glyph_asset_id: layer.glyph_asset_id, } } } @@ -65,7 +66,7 @@ pub struct SceneViewportLayerCache { logical_origin_y: i32, ring_origin_x: usize, ring_origin_y: usize, - pub glyph_bank_id: u8, + pub glyph_asset_id: AssetId, pub tile_size: TileSize, entries: Vec, pub valid: bool, @@ -80,7 +81,7 @@ impl SceneViewportLayerCache { logical_origin_y: 0, ring_origin_x: 0, ring_origin_y: 0, - glyph_bank_id: layer.glyph_bank_id, + glyph_asset_id: layer.glyph_asset_id, tile_size: layer.tile_size, entries: vec![CachedTileEntry::default(); width * height], valid: false, @@ -136,7 +137,7 @@ impl SceneViewportLayerCache { } pub fn refresh_region(&mut self, layer: &SceneLayer, region: ViewportRegion) { - self.glyph_bank_id = layer.glyph_bank_id; + self.glyph_asset_id = layer.glyph_asset_id; self.tile_size = layer.tile_size; let max_x = region.x.saturating_add(region.width).min(self.width); @@ -268,6 +269,7 @@ impl SceneViewportCache { #[cfg(test)] mod tests { use super::*; + use crate::asset::AssetId; use crate::glyph::Glyph; use crate::glyph_bank::TileSize; use crate::scene_layer::ParallaxFactor; @@ -278,13 +280,13 @@ mod tests { Tile { active: true, glyph: Glyph { glyph_id, palette_id }, flip_x, flip_y } } - fn make_layer(glyph_bank_id: u8, base_glyph: u16) -> SceneLayer { + fn make_layer(glyph_asset_id: AssetId, base_glyph: u16) -> SceneLayer { let mut tiles = Vec::new(); for y in 0..4 { for x in 0..4 { tiles.push(make_tile( base_glyph + (y * 4 + x) as u16, - glyph_bank_id, + 1, x % 2 == 0, y % 2 == 1, )); @@ -293,7 +295,7 @@ mod tests { SceneLayer { active: true, - glyph_bank_id, + glyph_asset_id, tile_size: TileSize::Size16, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width: 4, height: 4, tiles }, @@ -364,7 +366,7 @@ mod tests { assert!(entry.active); assert_eq!(entry.glyph_id, 105); assert_eq!(entry.palette_id, 1); - assert_eq!(entry.glyph_bank_id, 1); + assert_eq!(entry.glyph_asset_id, 1); assert!(!entry.flip_x()); assert!(entry.flip_y()); } diff --git a/crates/console/prometeu-hal/src/scene_viewport_resolver.rs b/crates/console/prometeu-hal/src/scene_viewport_resolver.rs index a9e99902..b2a65a50 100644 --- a/crates/console/prometeu-hal/src/scene_viewport_resolver.rs +++ b/crates/console/prometeu-hal/src/scene_viewport_resolver.rs @@ -386,6 +386,7 @@ impl SceneViewportResolver { #[cfg(test)] mod tests { use super::*; + use crate::asset::AssetId; use crate::glyph::Glyph; use crate::glyph_bank::TileSize; use crate::scene_layer::{ParallaxFactor, SceneLayer}; @@ -411,7 +412,7 @@ mod tests { SceneLayer { active: true, - glyph_bank_id: 1, + glyph_asset_id: 1 as AssetId, tile_size, parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y }, tilemap: TileMap { width, height, tiles }, 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 02ded60d..c5722e65 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -13,9 +13,20 @@ use prometeu_hal::{ AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int, }; +use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::Ordering; impl VirtualMachineRuntime { + fn panic_payload_to_string(payload: Box) -> String { + if let Some(message) = payload.downcast_ref::() { + return message.clone(); + } + if let Some(message) = payload.downcast_ref::<&str>() { + return (*message).to_string(); + } + "host panic without string payload".to_string() + } + fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> { let level = match level_val { 0 => LogLevel::Trace, @@ -177,7 +188,10 @@ impl NativeInterface for VirtualMachineRuntime { } }; - let status = if hw.bind_scene(scene_bank_id) { + let bound = catch_unwind(AssertUnwindSafe(|| hw.bind_scene(scene_bank_id))) + .map_err(|payload| VmFault::Panic(Self::panic_payload_to_string(payload)))?; + + let status = if bound { ComposerOpStatus::Ok } else { ComposerOpStatus::SceneUnavailable 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 dbe3eee5..e8152934 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -147,10 +147,10 @@ fn runtime_test_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> bank } -fn runtime_test_scene(glyph_bank_id: u8, palette_id: u8, tile_size: TileSize) -> SceneBank { +fn runtime_test_scene(glyph_asset_id: i32, palette_id: u8, tile_size: TileSize) -> SceneBank { let layer = SceneLayer { active: true, - glyph_bank_id, + glyph_asset_id, tile_size, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { @@ -294,6 +294,10 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() { 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)); + let glyph_slot_index = hardware.assets.glyph_asset_slot_index(); + let mut slots = [None; 16]; + slots[0] = Some(0); + glyph_slot_index.rebuild_from_slots(&slots); assert!(hardware.frame_composer.bind_scene(0)); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); @@ -348,6 +352,10 @@ fn tick_renders_scene_through_public_composer_syscalls() { 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)); + let glyph_slot_index = hardware.assets.glyph_asset_slot_index(); + let mut slots = [None; 16]; + slots[0] = Some(0); + glyph_slot_index.rebuild_from_slots(&slots); hardware.gfx.scene_fade_level = 31; hardware.gfx.hud_fade_level = 31; @@ -405,6 +413,10 @@ fn tick_draw_text_survives_scene_backed_frame_composition() { 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)); + let glyph_slot_index = hardware.assets.glyph_asset_slot_index(); + let mut slots = [None; 16]; + slots[0] = Some(0); + glyph_slot_index.rebuild_from_slots(&slots); hardware.gfx.scene_fade_level = 31; hardware.gfx.hud_fade_level = 31; 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 33191103..7078d428 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs @@ -4,9 +4,20 @@ use prometeu_hal::asset::{BankTelemetry, BankType}; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::{HardwareBridge, HostContext, InputSignals}; use prometeu_vm::LogicalFrameEndingReason; +use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::Ordering; impl VirtualMachineRuntime { + fn host_panic_payload_to_string(payload: Box) -> String { + if let Some(message) = payload.downcast_ref::() { + return message.clone(); + } + if let Some(message) = payload.downcast_ref::<&str>() { + return (*message).to_string(); + } + "host panic without string payload".to_string() + } + fn refresh_internal_allocation_telemetry(&self, vm: &VirtualMachine) { let heap_allocations = vm.heap().allocation_count().saturating_sub(self.frame_start_heap_allocations); @@ -167,7 +178,19 @@ impl VirtualMachineRuntime { if run.reason == LogicalFrameEndingReason::FrameSync || run.reason == LogicalFrameEndingReason::EndOfRom { - hw.render_frame(); + if let Err(payload) = catch_unwind(AssertUnwindSafe(|| hw.render_frame())) { + let message = Self::host_panic_payload_to_string(payload); + let report = + CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) }; + self.log( + LogLevel::Error, + LogSource::Vm, + report.log_tag(), + report.summary(), + ); + self.last_crash_report = Some(report.clone()); + return Some(report); + } // 1. Snapshot full telemetry at logical frame end let (glyph_bank, sound_bank, scene_bank) = Self::bank_telemetry_summary(hw); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 91a2e89b..413c30a0 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -21,7 +21,7 @@ {"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-0029","status":"open","ticket":"scene-bank-glyph-runtime-binding-leak","title":"Scene Bank Glyph Runtime Binding Leak","created_at":"2026-04-24","updated_at":"2026-04-24","tags":["gfx","runtime","asset","scene","glyph","format","architecture"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-scene-bank-glyph-runtime-binding-leak.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-24"}],"decisions":[{"id":"DEC-0021","file":"DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md","status":"in_progress","created_at":"2026-04-24","updated_at":"2026-04-24","ref_agenda":"AGD-0029"}],"plans":[{"id":"PLN-0040","file":"PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0041","file":"PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0042","file":"PLN-0042-scene-glyph-tooling-and-fixture-migration.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0029","status":"open","ticket":"scene-bank-glyph-runtime-binding-leak","title":"Scene Bank Glyph Runtime Binding Leak","created_at":"2026-04-24","updated_at":"2026-04-24","tags":["gfx","runtime","asset","scene","glyph","format","architecture"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-scene-bank-glyph-runtime-binding-leak.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-24"}],"decisions":[{"id":"DEC-0021","file":"DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md","status":"in_progress","created_at":"2026-04-24","updated_at":"2026-04-24","ref_agenda":"AGD-0029"}],"plans":[{"id":"PLN-0040","file":"PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0041","file":"PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0042","file":"PLN-0042-scene-glyph-tooling-and-fixture-migration.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]}],"lessons":[]} {"type":"discussion","id":"DSC-0014","status":"done","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0035","file":"lessons/DSC-0014-perf-vm-allocation-and-copy-pressure/LSN-0035-first-materialization-is-not-the-same-as-hot-path-copy-pressure.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]} {"type":"discussion","id":"DSC-0015","status":"abandoned","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"abandoned","created_at":"2026-03-27","updated_at":"2026-04-20","_override_reason":"User explicitly chose to close the discussion without decision because FS->memory copy for the program is already acceptable."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to abandon the discussion without creating a decision because FS->memory copy for the program is already acceptable."} {"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/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md b/discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md index 9bf062ce..26eae72e 100644 --- a/discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md +++ b/discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md @@ -2,9 +2,9 @@ id: PLN-0041 ticket: scene-bank-glyph-runtime-binding-leak title: Scene glyph runtime resolution, reverse index, and fatal paths -status: review +status: done created: 2026-04-24 -completed: +completed: 2026-04-24 tags: [gfx, runtime, asset, scene, glyph, render] --- @@ -138,4 +138,3 @@ Update the draw path so each layer resolves or revalidates the current glyph slo - Runtime fatalization may expose pre-existing tests or flows that assumed passive failure. - Reverse-index maintenance can become incorrect if any overwrite/clear path is missed. - Removing slot ids from scene/cache types can cascade into a larger render refactor than expected if code paths implicitly rely on them. - -- 2.47.2 From 4227d26101f68fb312ff307a9e9a86bcbd17b517 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 24 Apr 2026 14:15:31 +0100 Subject: [PATCH 4/8] implements PLN-0042 scene glyph tooling and fixtures --- .../prometeu-drivers/src/frame_composer.rs | 23 +++++++++++++++++++ crates/tools/pbxgen-stress/src/lib.rs | 11 +++++---- discussion/index.ndjson | 2 +- ...ene-glyph-tooling-and-fixture-migration.md | 5 ++-- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index faa1a00c..db4c62e5 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -847,4 +847,27 @@ mod tests { assert!(message.contains("SCENE dependency fatal during render_frame")); assert!(message.contains("glyph_asset_id=1")); } + + #[test] + fn render_frame_resolves_glyph_dependency_by_asset_id_not_by_slot_number() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(7, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE))); + banks.install_scene_bank(0, Arc::new(make_scene_with_palette(42, 2, TileSize::Size8))); + + let mut frame_composer = FrameComposer::new( + 16, + 16, + Arc::clone(&banks) as Arc, + make_glyph_slot_index(&[(42, 7)]), + ); + 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_eq!(gfx.front_buffer()[0], Color::BLUE.raw()); + } } diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index 7f165b8c..611689f0 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -4,8 +4,9 @@ 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, + AssetCodec, AssetEntry, AssetId, 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, @@ -338,7 +339,7 @@ fn build_scene_bank() -> SceneBank { SceneLayer { active: true, - glyph_bank_id: 0, + glyph_asset_id: 100, tile_size: TileSize::Size8, parallax_factor: match layer_index { 0 => ParallaxFactor { x: 1.0, y: 1.0 }, @@ -381,9 +382,9 @@ fn encode_scene_payload(scene: &SceneBank) -> Vec { 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.extend_from_slice(&(layer.glyph_asset_id as AssetId).to_le_bytes()); data.push(layer.tile_size as u8); - data.push(0); + data.extend_from_slice(&0_u16.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()); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 413c30a0..d4f0315e 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -21,7 +21,7 @@ {"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-0029","status":"open","ticket":"scene-bank-glyph-runtime-binding-leak","title":"Scene Bank Glyph Runtime Binding Leak","created_at":"2026-04-24","updated_at":"2026-04-24","tags":["gfx","runtime","asset","scene","glyph","format","architecture"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-scene-bank-glyph-runtime-binding-leak.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-24"}],"decisions":[{"id":"DEC-0021","file":"DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md","status":"in_progress","created_at":"2026-04-24","updated_at":"2026-04-24","ref_agenda":"AGD-0029"}],"plans":[{"id":"PLN-0040","file":"PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0041","file":"PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0042","file":"PLN-0042-scene-glyph-tooling-and-fixture-migration.md","status":"review","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0029","status":"open","ticket":"scene-bank-glyph-runtime-binding-leak","title":"Scene Bank Glyph Runtime Binding Leak","created_at":"2026-04-24","updated_at":"2026-04-24","tags":["gfx","runtime","asset","scene","glyph","format","architecture"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-scene-bank-glyph-runtime-binding-leak.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-24"}],"decisions":[{"id":"DEC-0021","file":"DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md","status":"in_progress","created_at":"2026-04-24","updated_at":"2026-04-24","ref_agenda":"AGD-0029"}],"plans":[{"id":"PLN-0040","file":"PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0041","file":"PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0042","file":"PLN-0042-scene-glyph-tooling-and-fixture-migration.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]}],"lessons":[]} {"type":"discussion","id":"DSC-0014","status":"done","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0035","file":"lessons/DSC-0014-perf-vm-allocation-and-copy-pressure/LSN-0035-first-materialization-is-not-the-same-as-hot-path-copy-pressure.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]} {"type":"discussion","id":"DSC-0015","status":"abandoned","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"abandoned","created_at":"2026-03-27","updated_at":"2026-04-20","_override_reason":"User explicitly chose to close the discussion without decision because FS->memory copy for the program is already acceptable."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to abandon the discussion without creating a decision because FS->memory copy for the program is already acceptable."} {"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/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md b/discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md index 8a844e23..6cf9a71a 100644 --- a/discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md +++ b/discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md @@ -2,9 +2,9 @@ id: PLN-0042 ticket: scene-bank-glyph-runtime-binding-leak title: Scene glyph tooling, fixtures, and test payload migration -status: review +status: done created: 2026-04-24 -completed: +completed: 2026-04-24 tags: [gfx, runtime, asset, scene, glyph, tooling, tests] --- @@ -113,4 +113,3 @@ Introduce tests that deliberately load glyph dependencies into different slots f - Test helper churn may be broad because many fixtures were written around the leaked slot model. - Tool outputs may silently drift if any scene payload writer is missed. - Regression tests can become brittle if they depend on incidental slot ordering instead of declared dependency identity. - -- 2.47.2 From 41801a8e7a6bd1bc0aaa8da4c1c399efba297832 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 24 Apr 2026 15:26:02 +0100 Subject: [PATCH 5/8] fix pbxgen stress scene glyph asset id mismatch --- crates/tools/pbxgen-stress/src/lib.rs | 19 ++++++++++++++----- test-cartridges/stress-console/assets.pa | Bin 35432 -> 35452 bytes 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index 611689f0..ff4b2c66 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -25,6 +25,9 @@ use std::fs; use std::mem::size_of; use std::path::PathBuf; +const STRESS_GLYPH_ASSET_ID: AssetId = 100; +const STRESS_SCENE_ASSET_ID: AssetId = 1; + fn asm(s: &str) -> Vec { assemble(s).expect("assemble") } @@ -224,7 +227,7 @@ fn build_assets_pack() -> Result> { let scene = build_scene_bank(); let scene_payload = encode_scene_payload(&scene); let scene_entry = AssetEntry { - asset_id: 1, + asset_id: STRESS_SCENE_ASSET_ID, asset_name: "stress_scene".into(), bank_type: BankType::SCENE, offset: glyph_payload.len() as u64, @@ -235,8 +238,10 @@ fn build_assets_pack() -> Result> { }; let asset_table = vec![glyph_entry, scene_entry]; - let preload = - vec![PreloadEntry { asset_id: 0, slot: 0 }, PreloadEntry { asset_id: 1, slot: 0 }]; + let preload = vec![ + PreloadEntry { asset_id: STRESS_GLYPH_ASSET_ID, slot: 0 }, + PreloadEntry { asset_id: STRESS_SCENE_ASSET_ID, 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; @@ -264,7 +269,7 @@ fn build_glyph_asset() -> (AssetEntry, Vec) { payload.extend_from_slice(&build_palette_bytes()); let entry = AssetEntry { - asset_id: 0, + asset_id: STRESS_GLYPH_ASSET_ID, asset_name: "stress_square".into(), bank_type: BankType::GLYPH, offset: 0, @@ -339,7 +344,7 @@ fn build_scene_bank() -> SceneBank { SceneLayer { active: true, - glyph_asset_id: 100, + glyph_asset_id: STRESS_GLYPH_ASSET_ID, tile_size: TileSize::Size8, parallax_factor: match layer_index { 0 => ParallaxFactor { x: 1.0, y: 1.0 }, @@ -431,8 +436,12 @@ mod tests { assert_eq!(header.asset_table.len(), 2); assert_eq!(header.preload.len(), 2); + assert_eq!(header.asset_table[0].asset_id, STRESS_GLYPH_ASSET_ID); + assert_eq!(header.asset_table[1].asset_id, STRESS_SCENE_ASSET_ID); 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].asset_id, STRESS_GLYPH_ASSET_ID); + assert_eq!(header.preload[1].asset_id, STRESS_SCENE_ASSET_ID); assert_eq!(header.preload[0].slot, 0); assert_eq!(header.preload[1].slot, 0); assert_eq!(header.asset_table[0].offset, 0); diff --git a/test-cartridges/stress-console/assets.pa b/test-cartridges/stress-console/assets.pa index 77c82efc858fcacad0ea558f4daa450b87705dba..ca43164c50f6549648ce3bc8bdc03c745d70fe71 100644 GIT binary patch delta 99 zcmaDch3U@}CSJ$j07pg!28Inl;=)8;eP%-ggUJGn;u}L)7?~{%EG9=Y`Y;(9Y+lY7 e#KD#VRLH@w`8TJO0*o!Lg<)|{f%}qjLa66Mw4S1eKxOV4B}v7;MmN}C8YqO Qm9-$WvloO;&XQCB0II4G!2kdN -- 2.47.2 From 92be3b90fbe9ac8eab38c983f457dbeeeffe3a89 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 24 Apr 2026 15:43:39 +0100 Subject: [PATCH 6/8] format scene glyph migration changes --- crates/console/prometeu-drivers/src/asset.rs | 81 +++++++++---------- .../prometeu-drivers/src/frame_composer.rs | 79 +++++++----------- crates/console/prometeu-drivers/src/gfx.rs | 15 ++-- crates/console/prometeu-hal/src/scene_bank.rs | 7 +- .../prometeu-hal/src/scene_viewport_cache.rs | 7 +- .../src/virtual_machine_runtime/dispatch.rs | 7 +- .../src/virtual_machine_runtime/tick.rs | 3 +- crates/tools/pbxgen-stress/src/lib.rs | 5 +- 8 files changed, 89 insertions(+), 115 deletions(-) diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index 2b1424d2..52d31d51 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -1186,47 +1186,46 @@ mod tests { } fn test_scene() -> SceneBank { - let make_layer = - |glyph_asset_id: AssetId, - active: bool, - parallax_x: f32, - parallax_y: f32, - tile_size: TileSize| SceneLayer { - active, - glyph_asset_id, - tile_size, - parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y }, - tilemap: TileMap { - width: 2, - height: 2, - tiles: vec![ - Tile { - active: true, - glyph: Glyph { glyph_id: 10, palette_id: 1 }, - flip_x: false, - flip_y: false, - }, - Tile { - active: true, - glyph: Glyph { glyph_id: 20, palette_id: 2 }, - flip_x: true, - flip_y: false, - }, - Tile { - active: true, - glyph: Glyph { glyph_id: 30, palette_id: 3 }, - flip_x: false, - flip_y: true, - }, - Tile { - active, - glyph: Glyph { glyph_id: 40, palette_id: 4 }, - flip_x: true, - flip_y: true, - }, - ], - }, - }; + let make_layer = |glyph_asset_id: AssetId, + active: bool, + parallax_x: f32, + parallax_y: f32, + tile_size: TileSize| SceneLayer { + active, + glyph_asset_id, + tile_size, + parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y }, + tilemap: TileMap { + width: 2, + height: 2, + tiles: vec![ + Tile { + active: true, + glyph: Glyph { glyph_id: 10, palette_id: 1 }, + flip_x: false, + flip_y: false, + }, + Tile { + active: true, + glyph: Glyph { glyph_id: 20, palette_id: 2 }, + flip_x: true, + flip_y: false, + }, + Tile { + active: true, + glyph: Glyph { glyph_id: 30, palette_id: 3 }, + flip_x: false, + flip_y: true, + }, + Tile { + active, + glyph: Glyph { glyph_id: 40, palette_id: 4 }, + flip_x: true, + flip_y: true, + }, + ], + }, + }; SceneBank { layers: [ diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index db4c62e5..0520c16a 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -252,7 +252,8 @@ impl FrameComposer { let scene_bank_id = self.active_scene_id.expect("active scene id must exist"); let resolved_glyph_slots = self.resolve_glyph_slots(scene, scene_bank_id, "render_frame"); - let (Some(cache), Some(resolver)) = (self.cache.as_mut(), self.resolver.as_mut()) else { + let (Some(cache), Some(resolver)) = (self.cache.as_mut(), self.resolver.as_mut()) + else { panic!("SCENE runtime invariant broken: active scene without cache/resolver"); }; let update = resolver.update(scene, self.camera_x_px, self.camera_y_px); @@ -342,8 +343,8 @@ mod tests { use crate::memory_banks::{ GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller, }; - use prometeu_hal::color::Color; use prometeu_hal::asset::AssetId; + use prometeu_hal::color::Color; use prometeu_hal::glyph_bank::{GlyphBank, TileSize}; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::tile::Tile; @@ -404,12 +405,8 @@ mod tests { #[test] fn frame_composer_starts_unbound_with_empty_owned_state() { - let frame_composer = FrameComposer::new( - 320, - 180, - Arc::new(MemoryBanks::new()), - make_glyph_slot_index(&[]), - ); + let frame_composer = + FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()), make_glyph_slot_index(&[])); assert_eq!(frame_composer.viewport_size(), (320, 180)); assert_eq!(frame_composer.active_scene_id(), None); @@ -479,12 +476,8 @@ mod tests { #[test] fn set_camera_stores_top_left_pixel_coordinates() { - let mut frame_composer = FrameComposer::new( - 320, - 180, - Arc::new(MemoryBanks::new()), - make_glyph_slot_index(&[]), - ); + let mut frame_composer = + FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()), make_glyph_slot_index(&[])); frame_composer.set_camera(-12, 48); @@ -506,12 +499,8 @@ mod tests { #[test] fn missing_scene_binding_falls_back_to_no_scene_state() { - let mut frame_composer = FrameComposer::new( - 320, - 180, - Arc::new(MemoryBanks::new()), - make_glyph_slot_index(&[]), - ); + let mut frame_composer = + FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()), make_glyph_slot_index(&[])); assert!(!frame_composer.bind_scene(7)); @@ -525,8 +514,7 @@ mod tests { let banks = Arc::new(MemoryBanks::new()); banks.install_scene_bank(0, Arc::new(make_scene())); - let mut frame_composer = - FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[])); + let mut frame_composer = FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[])); let panic = catch_unwind(AssertUnwindSafe(|| frame_composer.bind_scene(0))) .expect_err("bind_scene should panic when dependency is missing"); @@ -661,12 +649,8 @@ mod tests { #[test] fn frame_composer_emits_ordered_sprites_for_rendering() { - let mut frame_composer = FrameComposer::new( - 320, - 180, - Arc::new(MemoryBanks::new()), - make_glyph_slot_index(&[]), - ); + let mut frame_composer = + FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()), make_glyph_slot_index(&[])); frame_composer.begin_frame(); assert!(frame_composer.emit_sprite(Sprite { @@ -704,13 +688,12 @@ mod tests { 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, - make_glyph_slot_index(&[]), - ); + let mut frame_composer = FrameComposer::new( + 16, + 16, + Arc::clone(&banks) as Arc, + make_glyph_slot_index(&[]), + ); frame_composer.begin_frame(); assert!(frame_composer.emit_sprite(Sprite { glyph: Glyph { glyph_id: 0, palette_id: 3 }, @@ -740,13 +723,12 @@ mod tests { 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, - make_glyph_slot_index(&[(0, 0)]), - ); + let mut frame_composer = FrameComposer::new( + 16, + 16, + Arc::clone(&banks) as Arc, + make_glyph_slot_index(&[(0, 0)]), + ); assert!(frame_composer.bind_scene(0)); let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); @@ -776,13 +758,12 @@ mod tests { 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, - make_glyph_slot_index(&[(0, 0), (1, 1)]), - ); + let mut frame_composer = FrameComposer::new( + 16, + 16, + Arc::clone(&banks) as Arc, + make_glyph_slot_index(&[(0, 0), (1, 1)]), + ); let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); gfx.scene_fade_level = 31; gfx.hud_fade_level = 31; diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index cbf4579d..54689adb 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -909,8 +909,8 @@ impl Gfx { #[cfg(test)] mod tests { use super::*; - use crate::asset::GlyphAssetSlotIndex; use crate::FrameComposer; + use crate::asset::GlyphAssetSlotIndex; use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess}; use prometeu_hal::asset::AssetId; use prometeu_hal::glyph_bank::TileSize; @@ -1080,13 +1080,12 @@ mod tests { 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, - make_glyph_slot_index(&[]), - ); + let mut frame_composer = FrameComposer::new( + 32, + 18, + Arc::clone(&banks) as Arc, + make_glyph_slot_index(&[]), + ); gfx.begin_overlay_frame(); frame_composer.begin_frame(); diff --git a/crates/console/prometeu-hal/src/scene_bank.rs b/crates/console/prometeu-hal/src/scene_bank.rs index aa4d0d3a..67390aa4 100644 --- a/crates/console/prometeu-hal/src/scene_bank.rs +++ b/crates/console/prometeu-hal/src/scene_bank.rs @@ -15,7 +15,12 @@ mod tests { use crate::tile::Tile; use crate::tilemap::TileMap; - fn layer(glyph_asset_id: AssetId, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer { + fn layer( + glyph_asset_id: AssetId, + parallax_x: f32, + parallax_y: f32, + glyph_id: u16, + ) -> SceneLayer { SceneLayer { active: true, glyph_asset_id, diff --git a/crates/console/prometeu-hal/src/scene_viewport_cache.rs b/crates/console/prometeu-hal/src/scene_viewport_cache.rs index 8a7bf8ce..ca44a390 100644 --- a/crates/console/prometeu-hal/src/scene_viewport_cache.rs +++ b/crates/console/prometeu-hal/src/scene_viewport_cache.rs @@ -284,12 +284,7 @@ mod tests { let mut tiles = Vec::new(); for y in 0..4 { for x in 0..4 { - tiles.push(make_tile( - base_glyph + (y * 4 + x) as u16, - 1, - x % 2 == 0, - y % 2 == 1, - )); + tiles.push(make_tile(base_glyph + (y * 4 + x) as u16, 1, x % 2 == 0, y % 2 == 1)); } } 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 c5722e65..d3eff1b6 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -191,11 +191,8 @@ impl NativeInterface for VirtualMachineRuntime { let bound = catch_unwind(AssertUnwindSafe(|| hw.bind_scene(scene_bank_id))) .map_err(|payload| VmFault::Panic(Self::panic_payload_to_string(payload)))?; - let status = if bound { - ComposerOpStatus::Ok - } else { - ComposerOpStatus::SceneUnavailable - }; + let status = + if bound { ComposerOpStatus::Ok } else { ComposerOpStatus::SceneUnavailable }; ret.push_int(status as i64); Ok(()) } 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 7078d428..38c237f3 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs @@ -180,8 +180,7 @@ impl VirtualMachineRuntime { { if let Err(payload) = catch_unwind(AssertUnwindSafe(|| hw.render_frame())) { let message = Self::host_panic_payload_to_string(payload); - let report = - CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) }; + let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) }; self.log( LogLevel::Error, LogSource::Vm, diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index ff4b2c66..284f7b87 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -4,9 +4,8 @@ use prometeu_bytecode::model::{ BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl, }; use prometeu_hal::asset::{ - AssetCodec, AssetEntry, AssetId, BankType, PreloadEntry, - SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1, SCENE_LAYER_COUNT_V1, SCENE_PAYLOAD_MAGIC_V1, - SCENE_PAYLOAD_VERSION_V1, + AssetCodec, AssetEntry, AssetId, 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, -- 2.47.2 From 0c8de434cfe373da38f7c2dbc78142642c0b1ea2 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 24 Apr 2026 15:47:53 +0100 Subject: [PATCH 7/8] fix clippy warnings in scene and host paths --- crates/console/prometeu-drivers/src/asset.rs | 6 +- crates/console/prometeu-drivers/src/gfx.rs | 108 +++++++++--------- .../prometeu-host-desktop-winit/src/audio.rs | 1 + .../prometeu-host-desktop-winit/src/input.rs | 16 ++- .../prometeu-host-desktop-winit/src/runner.rs | 8 +- 5 files changed, 69 insertions(+), 70 deletions(-) diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index 52d31d51..6e67a941 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -1395,9 +1395,9 @@ mod tests { assert_eq!(decoded.layers[1].glyph_asset_id, 101); 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); - assert_eq!(decoded.layers[3].active, false); + assert!(decoded.layers[0].tilemap.tiles[1].flip_x); + assert!(decoded.layers[2].tilemap.tiles[2].flip_y); + assert!(!decoded.layers[3].active); } #[test] diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 54689adb..b74176f6 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -660,7 +660,9 @@ impl Gfx { self.back.fill(Color::BLACK.raw()); self.populate_layer_buckets(); - for layer_index in 0..cache.layers.len() { + for (layer_index, glyph_slot) in + resolved_glyph_slots.iter().enumerate().take(cache.layers.len()) + { Self::draw_bucket_on_buffer( &mut self.back, self.w, @@ -675,7 +677,7 @@ impl Gfx { self.h, cache, &update.copy_requests[layer_index], - resolved_glyph_slots[layer_index], + *glyph_slot, &*self.glyph_banks, ); } @@ -906,6 +908,56 @@ impl Gfx { } } +/// Blends in RGB565 per channel with saturation. +/// `dst` and `src` are RGB565 pixels (u16). +fn blend_rgb565(dst: u16, src: u16, mode: BlendMode) -> u16 { + match mode { + BlendMode::None => src, + + BlendMode::Half => { + let (dr, dg, db) = Color::unpack_to_native(dst); + let (sr, sg, sb) = Color::unpack_to_native(src); + let r = ((dr as u16 + sr as u16) >> 1) as u8; + let g = ((dg as u16 + sg as u16) >> 1) as u8; + let b = ((db as u16 + sb as u16) >> 1) as u8; + Color::pack_from_native(r, g, b) + } + + BlendMode::HalfPlus => { + let (dr, dg, db) = Color::unpack_to_native(dst); + let (sr, sg, sb) = Color::unpack_to_native(src); + + let r = (dr as u16 + ((sr as u16) >> 1)).min(31) as u8; + let g = (dg as u16 + ((sg as u16) >> 1)).min(63) as u8; + let b = (db as u16 + ((sb as u16) >> 1)).min(31) as u8; + + Color::pack_from_native(r, g, b) + } + + BlendMode::HalfMinus => { + let (dr, dg, db) = Color::unpack_to_native(dst); + let (sr, sg, sb) = Color::unpack_to_native(src); + + let r = (dr as i16 - ((sr as i16) >> 1)).max(0) as u8; + let g = (dg as i16 - ((sg as i16) >> 1)).max(0) as u8; + let b = (db as i16 - ((sb as i16) >> 1)).max(0) as u8; + + Color::pack_from_native(r, g, b) + } + + BlendMode::Full => { + let (dr, dg, db) = Color::unpack_to_native(dst); + let (sr, sg, sb) = Color::unpack_to_native(src); + + let r = (dr as u16 + sr as u16).min(31) as u8; + let g = (dg as u16 + sg as u16).min(63) as u8; + let b = (db as u16 + sb as u16).min(31) as u8; + + Color::pack_from_native(r, g, b) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -1055,9 +1107,7 @@ mod tests { 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(), 0); } @@ -1220,53 +1270,3 @@ mod tests { assert_eq!(gfx.sprites[1].glyph.glyph_id, 5); } } - -/// Blends in RGB565 per channel with saturation. -/// `dst` and `src` are RGB565 pixels (u16). -fn blend_rgb565(dst: u16, src: u16, mode: BlendMode) -> u16 { - match mode { - BlendMode::None => src, - - BlendMode::Half => { - let (dr, dg, db) = Color::unpack_to_native(dst); - let (sr, sg, sb) = Color::unpack_to_native(src); - let r = ((dr as u16 + sr as u16) >> 1) as u8; - let g = ((dg as u16 + sg as u16) >> 1) as u8; - let b = ((db as u16 + sb as u16) >> 1) as u8; - Color::pack_from_native(r, g, b) - } - - BlendMode::HalfPlus => { - let (dr, dg, db) = Color::unpack_to_native(dst); - let (sr, sg, sb) = Color::unpack_to_native(src); - - let r = (dr as u16 + ((sr as u16) >> 1)).min(31) as u8; - let g = (dg as u16 + ((sg as u16) >> 1)).min(63) as u8; - let b = (db as u16 + ((sb as u16) >> 1)).min(31) as u8; - - Color::pack_from_native(r, g, b) - } - - BlendMode::HalfMinus => { - let (dr, dg, db) = Color::unpack_to_native(dst); - let (sr, sg, sb) = Color::unpack_to_native(src); - - let r = (dr as i16 - ((sr as i16) >> 1)).max(0) as u8; - let g = (dg as i16 - ((sg as i16) >> 1)).max(0) as u8; - let b = (db as i16 - ((sb as i16) >> 1)).max(0) as u8; - - Color::pack_from_native(r, g, b) - } - - BlendMode::Full => { - let (dr, dg, db) = Color::unpack_to_native(dst); - let (sr, sg, sb) = Color::unpack_to_native(src); - - let r = (dr as u16 + sr as u16).min(31) as u8; - let g = (dg as u16 + sg as u16).min(63) as u8; - let b = (db as u16 + sb as u16).min(31) as u8; - - Color::pack_from_native(r, g, b) - } - } -} diff --git a/crates/host/prometeu-host-desktop-winit/src/audio.rs b/crates/host/prometeu-host-desktop-winit/src/audio.rs index 7018be08..6d381735 100644 --- a/crates/host/prometeu-host-desktop-winit/src/audio.rs +++ b/crates/host/prometeu-host-desktop-winit/src/audio.rs @@ -175,6 +175,7 @@ impl HostAudio { } #[cfg(test)] +#[allow(clippy::items_after_test_module)] mod tests { use super::*; use crate::stats::HostStats; diff --git a/crates/host/prometeu-host-desktop-winit/src/input.rs b/crates/host/prometeu-host-desktop-winit/src/input.rs index 45764a68..21e7334c 100644 --- a/crates/host/prometeu-host-desktop-winit/src/input.rs +++ b/crates/host/prometeu-host-desktop-winit/src/input.rs @@ -54,15 +54,13 @@ impl HostInputHandler { self.signals.y_pos = v.1; } - WindowEvent::MouseInput { state, button, .. } => { - if *button == MouseButton::Left { - match state { - ElementState::Pressed => { - self.signals.f_signal = true; - } - ElementState::Released => { - self.signals.f_signal = false; - } + WindowEvent::MouseInput { state, button, .. } if *button == MouseButton::Left => { + match state { + ElementState::Pressed => { + self.signals.f_signal = true; + } + ElementState::Released => { + self.signals.f_signal = false; } } } diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index a8b28e39..239b0c59 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -782,10 +782,10 @@ mod tests { break; } - if let Ok(resp) = serde_json::from_str::(&line) { - if resp["type"] == "getState" { - return; - } + if let Ok(resp) = serde_json::from_str::(&line) + && resp["type"] == "getState" + { + return; } } panic!("Did not receive getState response"); -- 2.47.2 From 1c91d1c2df78ab53f84e6f0a78f507cb8f7ae4ca Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 24 Apr 2026 15:57:52 +0100 Subject: [PATCH 8/8] housekeep DSC-0029 scene glyph binding leak discussion --- discussion/index.ndjson | 4 +- ...ependencies-must-bind-by-asset-identity.md | 70 +++++++ ...9-scene-bank-glyph-runtime-binding-leak.md | 193 ------------------ ...lyph-dependencies-must-bind-by-asset-id.md | 141 ------------- ...e-glyph-dependency-spec-and-wire-layout.md | 104 ---------- ...lyph-runtime-resolution-and-fatal-paths.md | 140 ------------- ...ene-glyph-tooling-and-fixture-migration.md | 115 ----------- 7 files changed, 72 insertions(+), 695 deletions(-) create mode 100644 discussion/lessons/DSC-0029-scene-bank-glyph-runtime-binding-leak/LSN-0038-cold-scene-dependencies-must-bind-by-asset-identity.md delete mode 100644 discussion/workflow/agendas/AGD-0029-scene-bank-glyph-runtime-binding-leak.md delete mode 100644 discussion/workflow/decisions/DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md delete mode 100644 discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md delete mode 100644 discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md delete mode 100644 discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index d4f0315e..5a1fa6fa 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":30,"AGD":30,"DEC":22,"PLN":43,"LSN":38,"CLSN":1}} +{"type":"meta","next_id":{"DSC":30,"AGD":30,"DEC":22,"PLN":43,"LSN":39,"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"}]} @@ -21,7 +21,7 @@ {"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-0029","status":"open","ticket":"scene-bank-glyph-runtime-binding-leak","title":"Scene Bank Glyph Runtime Binding Leak","created_at":"2026-04-24","updated_at":"2026-04-24","tags":["gfx","runtime","asset","scene","glyph","format","architecture"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-scene-bank-glyph-runtime-binding-leak.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-24"}],"decisions":[{"id":"DEC-0021","file":"DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md","status":"in_progress","created_at":"2026-04-24","updated_at":"2026-04-24","ref_agenda":"AGD-0029"}],"plans":[{"id":"PLN-0040","file":"PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0041","file":"PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]},{"id":"PLN-0042","file":"PLN-0042-scene-glyph-tooling-and-fixture-migration.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24","ref_decisions":["DEC-0021"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0029","status":"done","ticket":"scene-bank-glyph-runtime-binding-leak","title":"Scene Bank Glyph Runtime Binding Leak","created_at":"2026-04-24","updated_at":"2026-04-24","tags":["gfx","runtime","asset","scene","glyph","format","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0038","file":"lessons/DSC-0029-scene-bank-glyph-runtime-binding-leak/LSN-0038-cold-scene-dependencies-must-bind-by-asset-identity.md","status":"done","created_at":"2026-04-24","updated_at":"2026-04-24"}]} {"type":"discussion","id":"DSC-0014","status":"done","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0035","file":"lessons/DSC-0014-perf-vm-allocation-and-copy-pressure/LSN-0035-first-materialization-is-not-the-same-as-hot-path-copy-pressure.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]} {"type":"discussion","id":"DSC-0015","status":"abandoned","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"abandoned","created_at":"2026-03-27","updated_at":"2026-04-20","_override_reason":"User explicitly chose to close the discussion without decision because FS->memory copy for the program is already acceptable."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to abandon the discussion without creating a decision because FS->memory copy for the program is already acceptable."} {"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-0029-scene-bank-glyph-runtime-binding-leak/LSN-0038-cold-scene-dependencies-must-bind-by-asset-identity.md b/discussion/lessons/DSC-0029-scene-bank-glyph-runtime-binding-leak/LSN-0038-cold-scene-dependencies-must-bind-by-asset-identity.md new file mode 100644 index 00000000..b782c6fe --- /dev/null +++ b/discussion/lessons/DSC-0029-scene-bank-glyph-runtime-binding-leak/LSN-0038-cold-scene-dependencies-must-bind-by-asset-identity.md @@ -0,0 +1,70 @@ +--- +id: LSN-0038 +ticket: scene-bank-glyph-runtime-binding-leak +title: Cold Scene Dependencies Must Bind by Asset Identity, Not Runtime Residency +created: 2026-04-24 +tags: [gfx, runtime, asset, scene, glyph, format, architecture] +--- + +## Context + +`SCENE` payloads had started to serialize a per-layer `glyph_bank_id`, and the runtime was consuming that byte as if it were the authoritative glyph residency binding. That meant a cold scene asset was no longer just describing its dependencies; it was leaking the runtime slot topology that happened to exist when the asset was authored. + +The implementation work for `DSC-0029` replaced that contract end to end. Scene layers now carry glyph dependencies by canonical `AssetId (i32)`, the runtime owns the reverse residency lookup, and scene activation/composition fail fatally when required glyph assets are not currently resident. + +## Key Decisions + +### Scene Assets Must Describe Dependencies, Not Slot Topology + +**What:** +Each `SceneLayer` now declares its glyph dependency by canonical `AssetId`, and the `SCENE` wire format serializes that dependency as `i32`. + +**Why:** +Runtime slot ids are operational state. They can change with preload order, slot reuse, and runtime relocation. Putting them on the wire couples a cold asset to an unstable residency layout and breaks the intended separation between authored content and runtime execution state. + +**Trade-offs:** +The payload widened and old payload compatibility was intentionally dropped. That makes the migration more abrupt, but it restores the correct contract boundary instead of preserving an invalid one. + +### Runtime Residency Must Be Resolved Through Runtime-Owned Indexes + +**What:** +The runtime now owns the authoritative `asset_id -> slot` reverse index for committed glyph assets, and scene bind/draw paths consult that index instead of trusting scene bytes as slot bindings. + +**Why:** +Residency is a runtime concern. A reverse index gives the renderer and scene binder the correct primitive while keeping slot ownership inside the asset system where preload, commit, overwrite, and invalidation already live. + +**Trade-offs:** +The asset manager has more bookkeeping responsibility, but the model becomes coherent and future public lookup APIs can emerge from the same primitive. + +### Missing Scene Dependencies Are Fatal, Not Passive Status Errors + +**What:** +If `bind_scene` cannot resolve a declared glyph dependency, or if composition later loses a required dependency, the machine fails fatally with explicit logging. + +**Why:** +A bound scene without its declared glyph dependencies is not operationally meaningful. Treating that condition as a soft status error would let the system continue from a broken render contract. + +**Trade-offs:** +This is stricter than a passive operational rejection. The runtime becomes less forgiving, but it also becomes more honest: malformed residency state cannot masquerade as a valid scene activation path. + +## Patterns and Algorithms + +- Keep authored asset formats in cold identity space and runtime systems in residency space. +- Use `AssetId` as the handoff boundary between content and runtime resolution. +- Maintain reverse residency indexes inside the asset manager whenever runtime services need dependency lookup by identity. +- Re-resolve runtime bindings in the active path when residency can change after activation. +- Prefer fatal failure over passive degradation when a declared scene dependency is mandatory for correct composition. + +## Pitfalls + +- A one-byte field can look harmless while still encoding the wrong domain concept; width is less dangerous than semantics. +- Migrating the wire format without updating tooling and generated fixtures leaves the runtime correct but the stress assets incoherent. +- Freezing slot bindings too early would turn runtime residency changes into stale scene state. +- Introducing a reverse lookup only as an optimization shortcut invites future API and ownership problems; it should be modeled as a first-class runtime primitive. + +## Takeaways + +- Cold scene payloads should carry dependency identity, never runtime residency topology. +- Runtime slot lookup belongs to the asset manager, not to authored scene data. +- A reverse `asset_id -> slot` index is the clean bridge between authored dependencies and runtime composition. +- Missing scene dependencies should fail loudly because the render contract is broken, not merely unavailable. diff --git a/discussion/workflow/agendas/AGD-0029-scene-bank-glyph-runtime-binding-leak.md b/discussion/workflow/agendas/AGD-0029-scene-bank-glyph-runtime-binding-leak.md deleted file mode 100644 index c590fc23..00000000 --- a/discussion/workflow/agendas/AGD-0029-scene-bank-glyph-runtime-binding-leak.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -id: AGD-0029 -ticket: scene-bank-glyph-runtime-binding-leak -title: Scene Bank vazando glyph bank id runtime para o formato binario -status: accepted -created: 2026-04-24 -resolved: 2026-04-24 -decision: DEC-0021 -tags: [gfx, runtime, asset, scene, glyph, format, architecture] ---- - -## Contexto - -Hoje o payload binario `SCENE` carrega `glyph_bank_id` por layer e o runtime usa esse valor como chave operacional para resolver o `GlyphBank` residente durante o render. - -A cadeia atual eh: - -- o decoder de `SCENE` le `glyph_bank_id` do payload; -- `SceneLayer` preserva esse campo no modelo canonico carregado; -- `SceneViewportCache` copia o mesmo id para a cache; -- o renderer consulta `GlyphBankPoolAccess` por esse slot/id para desenhar a layer. - -Isso cria um acoplamento direto entre formato frio de asset e topologia de residencia runtime. A suspeita levantada eh que esse campo nao deveria existir no wire-format, porque a referencia correta ao glyph bank deveria ser resolvida apenas pelo runtime. - -## Problema - -O contrato atual mistura duas camadas que deveriam permanecer separadas: - -- o asset `SCENE` como descricao fria de composicao; -- o binding de slot/banco residente como estado operacional do runtime. - -Se `glyph_bank_id` realmente for um detalhe exclusivamente runtime, o formato binario atual esta errado e o runtime passou a depender de um identificador que nao deveria ser serializado nem publicado como parte do asset. - -Isso deixa perguntas abertas sobre: - -- como uma layer de cena passa a apontar para um glyph source sem carregar um slot runtime no payload; -- onde esse binding deve viver; -- como preload, install e bind de cena passam a cooperar sem reintroduzir ambiguidade. - -## Pontos Criticos - -- O renderer atual resolve a bank diretamente a partir de `layer_cache.glyph_bank_id`, entao qualquer correcao toca formato, modelo em memoria e caminho de render. -- O payload `SCENE` ja foi materializado em testes e tooling com esse byte no header da layer, entao a mudanca implica migracao/versionamento ou ruptura controlada. -- O contrato de asset management enfatiza slots como conceito runtime e resolve contexto de banco via `asset_table` e preload, o que conflita com `SCENE` carregar referencia operacional propria. -- Precisamos evitar uma "correcao" que apenas mova o mesmo acoplamento para outro nome sem restaurar a fronteira correta entre asset frio e binding runtime. - -## Opcoes - -### Opcao 1 - Manter `glyph_bank_id` dentro do formato `SCENE` - -- **Abordagem:** aceitar explicitamente que cada layer da cena referencia um slot/runtime glyph bank no proprio payload e documentar isso como contrato oficial. -- **Pros:** menor mudanca imediata; preserva compatibilidade com decoder, tests e tooling atuais. -- **Contras:** cristaliza o vazamento arquitetural; faz o asset depender de topologia runtime; dificulta relocacao, preload flexivel e reuso de cena em contextos diferentes. -- **Trade-off:** simples agora, mais rigido e mais fragil como contrato de longo prazo. - -### Opcao 2 - Remover o id do wire-format e resolver o binding por runtime ao ativar a cena - -- **Abordagem:** o payload `SCENE` deixa de carregar slot/id de glyph bank; a associacao layer -> fonte grafica passa a ser injetada por metadados de runtime, por preload derivado, ou por um binding explicito durante `bind_scene`. -- **Pros:** restaura a separacao asset frio vs residencia runtime; permite relocacao de slots e politica de preload sem contaminar o formato. -- **Contras:** exige definir uma nova fonte de verdade para o binding; impacta decoder, modelo de cena, renderer, specs e ferramentas. -- **Trade-off:** maior custo de migracao, mas fronteira arquitetural mais coerente. - -### Opcao 3 - Manter referencia serializada, mas trocar de slot runtime para identidade fria - -- **Abordagem:** o payload `SCENE` deixa de carregar `glyph_bank_id` runtime e passa a carregar uma referencia fria estavel por layer, como `asset_id` ou handle logico de glyph source; o runtime resolve essa identidade para slot residente quando a cena eh ativada. -- **Pros:** preserva auto-descricao da cena sem serializar topologia de residencia; reduz dependencia de slot fixo. -- **Contras:** ainda precisamos decidir se a cena deve conhecer assets frios diretamente; adiciona etapa de resolucao e falha operacional nova. -- **Trade-off:** melhor que serializar slot runtime, mas ainda pode acoplar demais o asset `SCENE` ao catalogo de assets. - -## Sugestao / Recomendacao - -Direcao escolhida nesta agenda: - -- seguir pela **Opcao 3** com identidade fria por `asset_id`; -- remover `glyph_bank_id` runtime do formato binario `SCENE`; -- fazer o `SCENE` carregar, por layer, o `asset_id` do glyph dependency; -- resolver `asset_id -> glyph slot` no momento de `bind_scene`; -- falhar operacionalmente se qualquer dependencia grafica da cena nao estiver comprometida em algum slot glyph; -- a ordem de carregamento dos assets nos bancos nao importa; -- **sem versionamento e sem compatibilidade retroativa por enquanto**. - -Motivo: - -- slot de glyph bank eh conceito runtime e nao deve viver no wire-format; -- `asset_id` eh identidade fria e estavel dentro do contrato de assets; -- bind-time resolution preserva a liberdade de preload/commit e evita acoplamento do asset `SCENE` a uma topologia fixa de slots; -- a falha operacional em `bind_scene` mantem o problema na fronteira correta de ativacao. - -Cravado nesta agenda: - -- `SceneBank` canonico nao deve carregar slot runtime de glyph; -- o payload `SCENE` deve declarar dependencias frias, nao residencia; -- a vinculacao layer -> glyph slot pertence ao runtime no momento de ativacao da cena; -- o runtime deve validar dependencias no `bind_scene`, mas nao deve persistir um snapshot resolvido como dado canonico da cena; -- no draw de cada layer, o runtime pode consultar novamente qual slot contem o `asset_id` referido pela cena; -- ausencia da dependencia em qualquer ponto relevante do fluxo deve produzir erro operacional. -- a resolucao `asset_id -> slot` fica interna nesta etapa; -- a resolucao interna deve ser suportada por indice reverso `asset_id -> slot`, nao depender apenas de scan linear dos slots; -- o payload `SCENE` deve aumentar a largura do campo para comportar `AssetId = i32`; -- cada layer publica sua propria dependencia glyph diretamente; -- falha de dependencia no caminho de draw/composition deve ser tratada como erro fatal da maquina, com log claro. -- falha de dependencia no `bind_scene` tambem deve ser tratada como erro fatal da maquina, com log claro. - -## Perguntas em Aberto - -## Criterio para Encerrar - -Esta agenda pode ser encerrada quando houver resposta clara e defendivel para: - -- como o `SCENE` publica dependencias por `asset_id`; -- como o runtime consulta `asset_id -> slot` durante bind e draw; -- qual representacao binaria carregara o `asset_id` por layer; -- como o contrato de erro fatal e log claro se aplica em bind e draw. - -## Discussao - -Estado inicial da investigacao: - -- ha vazamento real hoje no codigo e no tooling; -- nao encontrei decisao normativa registrada que feche explicitamente esse acoplamento; -- portanto o tema continua aberto e precisa de decisao arquitetural antes de qualquer correcao de spec ou codigo. - -Atualizacao de 2026-04-24: - -O caminho proposto pelo usuario parece tecnicamente viavel com a infraestrutura atual, embora ainda nao exista como contrato explicito. - -O que ja existe hoje: - -- o `AssetManager` mantem deduplicacao/residencia por `asset_id`; -- no commit/preload, cada slot fisico passa a registrar qual `asset_id` esta instalado nele; -- portanto o runtime ja possui as duas metades da informacao: - - `asset_id -> resident object`; - - `slot -> asset_id`. - -O que ainda nao existe: - -- uma API canonica `asset_id -> slot` para lookup de um asset ja comprometido em um banco; -- uma forma de `SceneBank` carregar dependencias por `asset_id` em vez de `glyph_bank_id`; -- um `bind_scene` capaz de validar todas as dependencias da cena e falhar operacionalmente se alguma nao estiver residente em slot algum. - -Leitura atual da proposta: - -- o payload `SCENE` carrega, por layer, o `asset_id` do glyph dependency; -- `composer.bind_scene(scene_slot)` continua selecionando a cena residente; -- durante o bind, o runtime resolve cada `asset_id` dependente para o slot glyph atualmente comprometido; -- durante o draw de cada layer, o runtime pode consultar novamente em qual slot o `asset_id` esta residente; -- a ordem de carregamento deixa de importar porque a resolucao eh feita contra o estado runtime no momento da operacao; -- se qualquer dependencia nao estiver carregada em algum slot glyph, `bind_scene` retorna falha operacional e nao ativa a cena. - -Essa direcao preserva a fronteira correta melhor do que o modelo atual: - -- o `SCENE` deixa de carregar topologia runtime; -- a cena continua auto-descritiva o suficiente para declarar suas dependencias frias; -- o runtime continua dono da residencia e da escolha de slot efetivo. - -Ponto de atencao: - -- essa direcao prefere nao manter snapshot resolvido como dado persistido da cena; -- portanto o contrato precisa dizer com clareza como o sistema reage se uma dependencia desaparecer entre bind e draw. - -Direcao aceita na discussao atual: - -- seguir com dependencia fria por `asset_id` no `SCENE`; -- resolver para slot glyph somente no `bind_scene`; -- permitir nova consulta `asset_id -> slot` tambem no draw das layers; -- nao expor lookup como API publica nesta etapa; -- manter indice reverso interno `asset_id -> slot` desde ja, para evitar acoplamento da solucao a scan linear de poucos slots; -- aumentar a largura do payload para carregar `AssetId = i32` no `SCENE`; -- tratar dependencia ausente no `bind_scene` como falha fatal da maquina, com log claro; -- tratar dependencia ausente no draw/composition como falha fatal da maquina, com log claro; -- nao fazer versionamento nem camada de compatibilidade nesta etapa. - -Resposta consolidada do usuario na discussao: - -- nao devemos materializar esse binding como dado resolvido persistente; -- devemos checar no bind e tambem quando formos desenhar a layer; -- a resolucao `asset_id -> slot` pode e deve evoluir para API publica futuramente, mas fica interna por enquanto; -- como essa resolucao tende a virar API publica, a base interna deve nascer como indice reverso e nao como mero loop sobre 16 slots; -- como `AssetId` canonico eh `i32`, o payload precisa aumentar a largura do campo serializado; -- cada layer carrega sua propria dependencia glyph; -- se a dependencia faltar ou nao puder ser resolvida no `bind_scene`, isso deve quebrar a maquina com log claro; -- se a dependencia sumir ou nao puder ser resolvida no caminho de draw/composition, isso tambem deve quebrar a maquina com log claro, porque nao ha como prosseguir corretamente sem as dependencias da cena. - -## Resolution - -Encerrada em 2026-04-24. - -Resolucao aceita: - -- `SCENE` deixa de carregar slot runtime de glyph e passa a carregar `AssetId` por layer; -- o runtime resolve `asset_id -> slot` via indice reverso interno; -- falta de dependencia fataliza a maquina tanto no `bind_scene` quanto no draw/composition, com log claro; -- nao ha versionamento nem compatibilidade retroativa nesta etapa. diff --git a/discussion/workflow/decisions/DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md b/discussion/workflow/decisions/DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md deleted file mode 100644 index 57c49b42..00000000 --- a/discussion/workflow/decisions/DEC-0021-scene-bank-glyph-dependencies-must-bind-by-asset-id.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -id: DEC-0021 -ticket: scene-bank-glyph-runtime-binding-leak -title: Scene Bank glyph dependencies MUST bind by asset_id, not runtime slot -status: in_progress -created: 2026-04-24 -accepted: 2026-04-24 -agenda: AGD-0029 -plans: [PLN-0040, PLN-0041, PLN-0042] -tags: [gfx, runtime, asset, scene, glyph, format, architecture] ---- - -## Status - -Accepted on 2026-04-24. - -## Contexto - -The current `SCENE` binary payload serializes `glyph_bank_id` per layer and the runtime uses that serialized value as the operational key to resolve the resident glyph bank during rendering. - -That design leaks runtime residency topology into a cold asset format. A scene asset is therefore coupled to whichever glyph slot layout happened to exist when the payload was authored, which violates the intended separation between: - -- cold asset description; -- runtime residency and slot assignment; -- frame-time composition. - -The repository discussion in `AGD-0029` concluded that this contract is incorrect and must be replaced. - -## Decisao - -The `SCENE` asset contract MUST stop serializing runtime glyph slot identifiers. - -From this decision forward: - -1. Each scene layer MUST declare its glyph dependency by `AssetId`. -2. `SceneBank` MUST NOT carry a runtime glyph slot id as part of its canonical loaded model. -3. The runtime MUST resolve `AssetId -> glyph slot` through runtime-owned residency state, not through scene payload bytes interpreted as slot ids. -4. The runtime MUST maintain an internal reverse index `AssetId -> slot` for committed glyph assets. -5. The lookup mechanism MAY become a public API later, but in this stage it MUST remain internal. -6. The `SCENE` payload field used for the glyph dependency MUST widen to carry canonical `AssetId = i32`. -7. The scene payload MUST keep the dependency attached directly to each layer; no shared dependency table is introduced in this decision. -8. No compatibility layer and no format version migration are introduced in this stage. - -## Rationale - -The previous contract was wrong at the boundary level, not only at the implementation level. - -Why this decision is correct: - -- runtime slot ids are operational state and MUST remain runtime-owned; -- `AssetId` is the stable cold identity already used by the asset system; -- resolving dependencies at runtime preserves freedom of preload, commit, relocation, and slot reuse; -- a reverse index gives the runtime the correct primitive for both internal use and possible future API exposure; -- keeping the dependency per layer is the lowest-risk migration path and avoids speculative structure growth. - -This decision deliberately prefers a clean contract over temporary compatibility because the current behavior is architecturally invalid. - -## Invariantes / Contrato - -### Canonical scene contract - -- A `SCENE` asset MUST describe glyph dependencies in cold asset terms only. -- A `SCENE` asset MUST NOT encode runtime slot topology. -- A scene layer MUST own exactly one glyph dependency reference. -- That dependency reference MUST be serialized as `AssetId` and MUST use `i32` width. - -### Runtime residency contract - -- The asset manager MUST own the authoritative mapping from committed glyph residency to slot. -- The runtime MUST maintain a reverse index from committed glyph `AssetId` to glyph slot. -- The reverse index MUST be updated whenever glyph preload or glyph commit changes slot ownership. -- The reverse index MUST be treated as internal runtime state in this stage. - -### Bind-time and draw-time contract - -- `bind_scene` MUST validate that every glyph dependency referenced by the target scene resolves to a currently committed glyph slot. -- If any dependency cannot be resolved during `bind_scene`, the machine MUST fail fatally and emit a clear log describing the missing dependency. -- Render-time composition MAY consult the reverse index again while drawing a layer instead of relying on a frozen slot snapshot. -- If a dependency becomes unresolved during draw/composition, the machine MUST fail fatally and emit a clear log describing the missing dependency. -- The runtime MUST NOT continue scene composition after a missing dependency is detected at bind time or draw time. - -### Error semantics - -- `ERROR_MISSING_DEPENDENCIES` is not part of the accepted contract. -- Missing scene glyph dependencies are not passive operational rejections in the accepted model. -- Missing scene glyph dependencies MUST escalate as fatal machine errors with explicit logging. - -### Non-goals for this decision - -- This decision does NOT expose the reverse lookup as a public API yet. -- This decision does NOT introduce payload compatibility with the old serialized `glyph_bank_id`. -- This decision does NOT introduce a dependency table shared across scene layers. -- This decision does NOT define the later public guest ABI for querying `asset_id -> slot`. - -## Impactos - -### Spec - -- The `SCENE` binary payload specification MUST be updated to replace per-layer runtime slot ids with per-layer `AssetId`. -- The payload layout MUST reflect widened field width for `AssetId = i32`. -- Runtime/spec text that implies scene glyph dependencies are operational status returns rather than fatal errors MUST be corrected. - -### Runtime - -- `SceneLayer` and related scene/cache structures MUST stop carrying canonical runtime glyph slot ids. -- The asset manager MUST maintain the reverse glyph residency index. -- `bind_scene` MUST validate scene glyph dependencies against the reverse index and fatalize on absence. -- The draw path MUST resolve or revalidate layer dependencies against runtime residency and fatalize on absence. - -### Host - -- Fatal machine logging for missing scene glyph dependencies MUST be clear enough to identify: - - the scene being bound or drawn; - - the layer involved; - - the missing glyph dependency `AssetId`. - -### Tooling - -- Scene encoders, packers, fixtures, and stress tools MUST serialize per-layer glyph dependency as `AssetId`, not runtime slot id. -- Existing tests and fixtures that publish `glyph_bank_id` in `SCENE` payloads MUST be rewritten. - -## Referencias - -- Agenda: `AGD-0029-scene-bank-glyph-runtime-binding-leak.md` -- Related runtime area: `DSC-0025` lesson on scene/canonical/cache separation -- Related asset identity context: `DSC-0018` lesson on asset load asset id int contract - -## Propagacao Necessaria - -- Update the canonical `SCENE` payload layout in specs. -- Update HAL scene types to reflect `AssetId` dependency semantics. -- Update asset decode/encode logic for `SCENE`. -- Introduce and maintain reverse glyph residency index in the asset manager. -- Update `FrameComposer` bind path and draw path to use runtime dependency resolution and fatal logging. -- Rewrite affected tests, fixtures, and tooling payload writers. -- Create an execution plan before spec/code changes. - -## Revision Log - -- 2026-04-24: Initial accepted decision derived from AGD-0029. -- 2026-04-24: Moved to in_progress after plan family creation (`PLN-0040`, `PLN-0041`, `PLN-0042`). diff --git a/discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md b/discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md deleted file mode 100644 index 34e13a0e..00000000 --- a/discussion/workflow/plans/PLN-0040-scene-glyph-dependency-spec-and-wire-layout.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -id: PLN-0040 -ticket: scene-bank-glyph-runtime-binding-leak -title: Scene glyph dependency spec and wire layout migration -status: done -created: 2026-04-24 -completed: 2026-04-24 -tags: [gfx, runtime, asset, scene, glyph, format, spec] ---- - -## Briefing - -Implement the specification-side portion of `DEC-0021` by rewriting the `SCENE` payload contract so that each scene layer serializes a glyph dependency by canonical `AssetId (i32)` instead of a runtime glyph slot id. This plan covers only normative docs and wire-layout definition. It does not implement runtime code. - -## Decisions de Origem - -- `DEC-0021` — Scene Bank glyph dependencies MUST bind by `asset_id`, not runtime slot. - -## Alvo - -- Canonical runtime specs under `docs/specs/runtime/`. -- Shared payload constants and comments under `crates/console/prometeu-hal/` only where needed to reflect the normative wire layout. - -## Escopo - -- Rewrite the `SCENE` payload description to replace per-layer runtime slot semantics with per-layer `AssetId`. -- Define widened field width for serialized scene glyph dependencies. -- Remove any spec language implying passive missing-dependency status for scene composition. -- Align related comments/constants with the new payload field width and meaning. - -## Fora de Escopo - -- Runtime bind/draw implementation. -- Reverse index implementation in `AssetManager`. -- Test fixture rewrites and tool migration. -- Any compatibility or versioning path for the old payload. - -## Plano de Execucao - -### Step 1 - Rewrite the normative `SCENE` payload contract - -**What:** -Define the scene-layer dependency field as `AssetId (i32)` and remove all remaining wording that treats scene glyph linkage as runtime slot topology. - -**How:** -Update the relevant `docs/specs/runtime` sections so the payload layout, field meaning, and operational consequences all match `DEC-0021`. State explicitly that the scene asset is cold-description only and MUST NOT encode runtime slot ids. - -**File(s):** -- `docs/specs/runtime/15-asset-management.md` -- any scene/gfx spec chapter that documents `SCENE` payload fields or scene dependency semantics. - -### Step 2 - Update shared HAL wire-layout constants and inline documentation - -**What:** -Bring shared constants and comments into sync with the widened scene dependency field. - -**How:** -Review `crates/console/prometeu-hal/src/asset.rs` and any nearby scene-related code comments. Update layer-header sizing constants and field comments if the payload header width changes from the previous single-byte dependency representation. - -**File(s):** -- `crates/console/prometeu-hal/src/asset.rs` -- neighboring scene payload comments when affected. - -### Step 3 - Tighten the published fatal-error semantics - -**What:** -Make the public documentation consistent with the accepted fatal behavior for missing scene glyph dependencies. - -**How:** -Remove proposed/legacy wording that frames missing dependencies as recoverable status returns for scene activation/composition. Describe the bind-time and draw-time dependency absence as fatal machine errors with clear logging expectations. - -**File(s):** -- `docs/specs/runtime/04-gfx-peripheral.md` -- `docs/specs/runtime/16-host-abi-and-syscalls.md` -- any other spec chapter that describes `composer.bind_scene`. - -## Criterios de Aceite - -- [ ] The published `SCENE` payload contract defines per-layer glyph dependency as `AssetId (i32)`. -- [ ] No normative spec text claims that `SCENE` serializes runtime glyph slot ids. -- [ ] No normative spec text retains `ERROR_MISSING_DEPENDENCIES` as the accepted contract for scene dependency failures. -- [ ] Shared HAL constants/comments reflect the revised payload layout where applicable. - -## Tests / Validacao - -### Unit Tests - -- Add or update compile-time/runtime assertions only if shared payload constants are exercised by existing tests. - -### Integration Tests - -- None in this plan; verification is doc/contract oriented. - -### Manual Verification - -- Review the updated spec text to ensure every `SCENE` glyph dependency reference uses `AssetId` language. -- Verify that the documented payload width matches `AssetId = i32`. -- Verify that fatal semantics for missing dependencies are stated consistently across spec chapters. - -## Riscos - -- Spec-only edits can drift from the runtime implementation if later plans are not executed promptly. -- Scene payload layout changes may require touching multiple docs; missing one will leave contradictory contract text. -- If HAL constants are updated here before runtime code, temporary compile or test fallout may appear in later plans and must be handled intentionally. diff --git a/discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md b/discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md deleted file mode 100644 index 26eae72e..00000000 --- a/discussion/workflow/plans/PLN-0041-scene-glyph-runtime-resolution-and-fatal-paths.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -id: PLN-0041 -ticket: scene-bank-glyph-runtime-binding-leak -title: Scene glyph runtime resolution, reverse index, and fatal paths -status: done -created: 2026-04-24 -completed: 2026-04-24 -tags: [gfx, runtime, asset, scene, glyph, render] ---- - -## Briefing - -Implement the runtime half of `DEC-0021`: remove canonical runtime glyph slot ids from scene types, widen scene dependency serialization to `AssetId (i32)`, introduce and maintain the internal reverse glyph residency index, and enforce fatal machine behavior when scene dependencies are missing at bind time or draw time. - -## Decisions de Origem - -- `DEC-0021` — Scene Bank glyph dependencies MUST bind by `asset_id`, not runtime slot. - -## Alvo - -- `crates/console/prometeu-hal/` -- `crates/console/prometeu-drivers/` -- `crates/console/prometeu-system/` only if runtime-visible behavior or tests require updates. - -## Escopo - -- Replace scene-layer glyph slot semantics with `AssetId` semantics in HAL/runtime types. -- Widen `SCENE` decode/encode logic to serialize `AssetId (i32)` per layer. -- Maintain an internal reverse index `AssetId -> glyph slot` for committed glyph assets. -- Enforce fatal handling with explicit logging when dependencies are missing during `bind_scene` or draw/composition. - -## Fora de Escopo - -- Public guest ABI for reverse lookup. -- Compatibility with old `glyph_bank_id` scene payloads. -- Tooling/stress-fixture migration beyond the runtime code needed to compile. - -## Plano de Execucao - -### Step 1 - Change canonical scene types to cold dependency identity - -**What:** -Remove canonical runtime glyph slot ids from scene-layer related types and replace them with glyph dependency `AssetId`. - -**How:** -Refactor `SceneLayer`, scene cache entries, viewport cache state, resolver helpers, and tests so the scene model stores dependency identity rather than slot identity. Preserve only the information needed for cold scene description plus runtime materialization. - -**File(s):** -- `crates/console/prometeu-hal/src/scene_layer.rs` -- `crates/console/prometeu-hal/src/scene_viewport_cache.rs` -- `crates/console/prometeu-hal/src/scene_viewport_resolver.rs` -- scene-related tests under `prometeu-hal`. - -### Step 2 - Update `SCENE` decode/encode path to `AssetId (i32)` - -**What:** -Rewrite the binary `SCENE` decoder and local encoders to read/write `AssetId` instead of single-byte glyph slot ids. - -**How:** -Adjust header width constants, field offsets, and validation in `AssetManager::decode_scene_bank_*` and any local test encoders. Ensure decode rejects malformed payloads under the new layout and that any helper fixture builder writes `i32` dependencies per layer. - -**File(s):** -- `crates/console/prometeu-drivers/src/asset.rs` -- any scene payload helpers under tests. - -### Step 3 - Introduce reverse glyph residency index - -**What:** -Add a runtime-owned reverse mapping from committed glyph `AssetId` to glyph slot. - -**How:** -Extend `AssetManager` so glyph preload, glyph commit, shutdown, and any slot replacement paths update both: -- the existing `slot -> asset_id` tracking; -- the new reverse `asset_id -> slot` index. - -Keep this mechanism internal in this stage. Remove stale entries when a slot is overwritten or cleared. - -**File(s):** -- `crates/console/prometeu-drivers/src/asset.rs` -- `crates/console/prometeu-hal/src/asset_bridge.rs` only if an internal-facing trait hook is required. - -### Step 4 - Enforce fatal dependency validation in `bind_scene` - -**What:** -Make scene activation fail fatally when any scene glyph dependency does not resolve to a committed glyph slot. - -**How:** -Update the `FrameComposer` bind path and any caller path so bind checks every layer dependency against the reverse index before activating the scene. Emit a clear fatal log naming the scene context, layer index, and missing dependency `AssetId`. - -**File(s):** -- `crates/console/prometeu-drivers/src/frame_composer.rs` -- `crates/console/prometeu-drivers/src/hardware.rs` -- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` if behavior shaping is needed. - -### Step 5 - Enforce fatal dependency validation in draw/composition - -**What:** -Make render-time scene composition fail fatally if a previously bindable dependency later becomes unresolved. - -**How:** -Update the draw path so each layer resolves or revalidates the current glyph slot for the layer dependency through the reverse index instead of trusting old scene slot data. If resolution fails, emit the same class of fatal log and abort machine progress. - -**File(s):** -- `crates/console/prometeu-drivers/src/gfx.rs` -- `crates/console/prometeu-drivers/src/frame_composer.rs` - -## Criterios de Aceite - -- [ ] No canonical scene type stores runtime glyph slot id as scene dependency state. -- [ ] `SCENE` decode/encode paths serialize per-layer dependency as `AssetId (i32)`. -- [ ] `AssetManager` maintains a reverse glyph index updated by preload and commit. -- [ ] `bind_scene` fatalizes with clear logging when a dependency is missing. -- [ ] Draw/composition fatalizes with clear logging when a dependency is missing. -- [ ] No runtime path continues scene composition after missing dependency detection. - -## Tests / Validacao - -### Unit Tests - -- Add/update tests for scene decode under the widened `AssetId` field. -- Add/update tests for reverse index maintenance across preload, commit, overwrite, and shutdown. -- Add/update tests for `FrameComposer` bind failure on missing glyph dependency. - -### Integration Tests - -- Add/update runtime integration tests covering: - - scene bind with all dependencies present; - - fatal bind when a dependency is absent; - - fatal draw when a dependency becomes unresolved after bind, if that transition is constructible in tests. - -### Manual Verification - -- Load a cartridge with scene and glyph dependencies committed in non-matching slot order and confirm bind still resolves correctly. -- Confirm fatal logs identify the missing `AssetId` and layer when dependency resolution fails. - -## Riscos - -- Runtime fatalization may expose pre-existing tests or flows that assumed passive failure. -- Reverse-index maintenance can become incorrect if any overwrite/clear path is missed. -- Removing slot ids from scene/cache types can cascade into a larger render refactor than expected if code paths implicitly rely on them. diff --git a/discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md b/discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md deleted file mode 100644 index 6cf9a71a..00000000 --- a/discussion/workflow/plans/PLN-0042-scene-glyph-tooling-and-fixture-migration.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -id: PLN-0042 -ticket: scene-bank-glyph-runtime-binding-leak -title: Scene glyph tooling, fixtures, and test payload migration -status: done -created: 2026-04-24 -completed: 2026-04-24 -tags: [gfx, runtime, asset, scene, glyph, tooling, tests] ---- - -## Briefing - -Complete the implementation surface of `DEC-0021` by migrating all scene payload producers, fixtures, stress tools, and test data away from serialized runtime glyph slots and onto per-layer glyph dependency `AssetId (i32)`. - -## Decisions de Origem - -- `DEC-0021` — Scene Bank glyph dependencies MUST bind by `asset_id`, not runtime slot. - -## Alvo - -- Runtime tests and helpers under `crates/console/prometeu-drivers/` -- Runtime tests under `crates/console/prometeu-system/` -- Tooling and stress generators under `crates/tools/` - -## Escopo - -- Rewrite scene payload writers in tests and tools. -- Rewrite test fixtures and expectations that still name or assert `glyph_bank_id`. -- Ensure stress and fixture content uses valid `AssetId` dependencies and exercises runtime resolution order independence. - -## Fora de Escopo - -- Core runtime implementation of reverse index and fatal semantics. -- Public API exposure of reverse lookup. -- Backward-compatible scene payload generation. - -## Plano de Execucao - -### Step 1 - Rewrite local scene payload builders in driver tests - -**What:** -Update every driver-side scene helper and binary payload builder that still writes per-layer runtime slot ids. - -**How:** -Search all `SceneLayer` builders and `SCENE` payload encoders in driver tests. Replace slot-oriented setup with per-layer dependency `AssetId`, then adjust assertions accordingly. - -**File(s):** -- `crates/console/prometeu-drivers/src/asset.rs` -- `crates/console/prometeu-drivers/src/frame_composer.rs` -- `crates/console/prometeu-drivers/src/gfx.rs` -- `crates/console/prometeu-drivers/src/hardware.rs` - -### Step 2 - Rewrite system/runtime integration fixtures - -**What:** -Migrate integration fixtures so scene setup reflects asset-id dependencies and runtime slot resolution. - -**How:** -Update helper functions and tests under `prometeu-system` that construct scenes or assume direct slot semantics. Ensure tests explicitly preload/commit glyph assets under varying slot orders and verify correct resolution behavior. - -**File(s):** -- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs` -- other runtime test modules that build scene fixtures. - -### Step 3 - Rewrite tooling and stress generators - -**What:** -Update scene-producing tools so published binary payloads match the accepted scene dependency contract. - -**How:** -Rewrite stress/payload builders to write `AssetId (i32)` per layer and to generate coherent asset tables/preload data that prove runtime resolution by dependency identity rather than baked slot topology. - -**File(s):** -- `crates/tools/pbxgen-stress/src/lib.rs` -- any other local scene payload generators discovered during implementation. - -### Step 4 - Add regression coverage for former slot leakage - -**What:** -Add focused regression cases that fail if scene payloads again leak runtime glyph slot ids. - -**How:** -Introduce tests that deliberately load glyph dependencies into different slots from their prior fixture order and verify scene bind/draw behavior depends on `AssetId` resolution only. - -**File(s):** -- affected test modules under `prometeu-drivers`, `prometeu-system`, and tool tests if present. - -## Criterios de Aceite - -- [ ] No test or tooling payload writer serializes per-layer runtime glyph slot id for `SCENE`. -- [ ] Scene fixtures express glyph dependencies by `AssetId`. -- [ ] At least one regression test proves glyph slot order does not matter for scene activation. -- [ ] At least one regression test would fail if old slot-leak behavior returned. - -## Tests / Validacao - -### Unit Tests - -- Update existing scene codec/fixture tests to assert `AssetId`-based dependency behavior. - -### Integration Tests - -- Run runtime integration coverage where scene dependencies resolve correctly despite different glyph slot placement. -- Run regression coverage for fatal behavior when dependencies are absent. - -### Manual Verification - -- Inspect representative generated payload bytes to confirm widened dependency field writing. -- Verify stress/tool outputs remain consumable by the updated runtime. - -## Riscos - -- Test helper churn may be broad because many fixtures were written around the leaked slot model. -- Tool outputs may silently drift if any scene payload writer is missed. -- Regression tests can become brittle if they depend on incidental slot ordering instead of declared dependency identity. -- 2.47.2