From 38a3edc0fd7d0fe3fd7a78768c362af85a1a9b51 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 08:06:04 +0100 Subject: [PATCH 01/47] Housekeeping --- ...-submissions-preserve-domain-boundaries.md | 54 +++ ...packet-boundary-for-classic-2d-renderer.md | 446 ------------------ ...ogical-render-pipeline-command-boundary.md | 236 --------- .../plans/PLN-0073-render-contract-specs.md | 67 --- .../PLN-0074-hal-render-submission-types.md | 66 --- .../plans/PLN-0075-rendermanager-core.md | 68 --- ...-0076-capabilities-and-abi-domain-split.md | 67 --- ...-0077-composer-buffer-and-game2d-packet.md | 66 --- ...N-0078-classic2d-game-renderer-consumer.md | 64 --- .../plans/PLN-0079-gfx2d-primitive-domain.md | 66 --- ...N-0080-shell-ui-packet-and-gfxui-domain.md | 66 --- .../plans/PLN-0081-shell-hub-migration.md | 67 --- ...-frame-publication-and-present-boundary.md | 69 --- .../workflow/plans/PLN-0083-fade-removal.md | 63 --- .../PLN-0084-host-debug-overlay-removal.md | 65 --- ...085-pbs-stdlib-and-syscall-declarations.md | 67 --- ...6-end-to-end-render-boundary-validation.md | 71 --- 17 files changed, 54 insertions(+), 1614 deletions(-) create mode 100644 discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md delete mode 100644 discussion/workflow/agendas/AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md delete mode 100644 discussion/workflow/decisions/DEC-0030-logical-render-pipeline-command-boundary.md delete mode 100644 discussion/workflow/plans/PLN-0073-render-contract-specs.md delete mode 100644 discussion/workflow/plans/PLN-0074-hal-render-submission-types.md delete mode 100644 discussion/workflow/plans/PLN-0075-rendermanager-core.md delete mode 100644 discussion/workflow/plans/PLN-0076-capabilities-and-abi-domain-split.md delete mode 100644 discussion/workflow/plans/PLN-0077-composer-buffer-and-game2d-packet.md delete mode 100644 discussion/workflow/plans/PLN-0078-classic2d-game-renderer-consumer.md delete mode 100644 discussion/workflow/plans/PLN-0079-gfx2d-primitive-domain.md delete mode 100644 discussion/workflow/plans/PLN-0080-shell-ui-packet-and-gfxui-domain.md delete mode 100644 discussion/workflow/plans/PLN-0081-shell-hub-migration.md delete mode 100644 discussion/workflow/plans/PLN-0082-frame-publication-and-present-boundary.md delete mode 100644 discussion/workflow/plans/PLN-0083-fade-removal.md delete mode 100644 discussion/workflow/plans/PLN-0084-host-debug-overlay-removal.md delete mode 100644 discussion/workflow/plans/PLN-0085-pbs-stdlib-and-syscall-declarations.md delete mode 100644 discussion/workflow/plans/PLN-0086-end-to-end-render-boundary-validation.md diff --git a/discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md b/discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md new file mode 100644 index 00000000..f4d5b677 --- /dev/null +++ b/discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md @@ -0,0 +1,54 @@ +--- +id: LSN-0047 +ticket: render-frame-packet-boundary +title: Typed Render Submissions Preserve Domain Boundaries +created: 2026-06-04 +tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] +--- + +# LSN-0047: Typed Render Submissions Preserve Domain Boundaries + +## Context + +The RGBA8888 migration made the framebuffer format explicit, but the runtime still needed a cleaner boundary between logical frame production and surface publication. The important follow-up was not a GPU backend, a render thread, or a universal display list. It was the separation between domain-owned logical buffers, closed render submissions, and the surface implementation that turns those submissions into pixels. + +`DSC-0038` closed that boundary across specs, runtime types, mode routing, PBS declarations, shell migration, host overlay removal, and validation. The resulting model treats rendering as a runtime contract above the final desktop surface rather than as direct mutable access to `Gfx` or a framebuffer. + +## Key Decisions + +### Logical Render Pipeline Command Boundary + +**What:** Prometeu introduced a runtime-level `RenderManager` that closes mode-appropriate domain buffers into immutable `RenderSubmission` snapshots. A submission carries `frame_id`, `app_mode`, and a typed packet such as `Game2DFramePacket` or `UiFramePacket`. + +**Why:** Game rendering and shell UI have different domain meanings. Game 2D owns scene, camera, sprites, HUD, and game primitives. Shell UI owns system-facing UI primitives. A weak universal command list would hide those differences and make future pipelines fit an accidental 2D/UI shape. + +**Trade-offs:** The typed packet model creates more explicit types and routing work up front. In return, it keeps ABI domains honest, lets each pipeline evolve independently, and gives host/render-surface code a closed snapshot that can later be consumed off the VM hot path. + +## Patterns and Algorithms + +Keep three concepts separate: + +1. Domain buffers are mutable while a logical frame is being built. +2. `RenderSubmission` is the immutable snapshot produced at the frame boundary. +3. Render-surface implementations consume submissions and publish RGBA8888 pixels. + +Use `RenderManager` for coordination, not drawing. It owns active app mode, submission closure, transition policy, capabilities, and publication flow. It must not become the desktop renderer, the shell UI implementation, or the game composer. + +Prefer typed top-level packets over a universal command list when pipelines carry different semantic contracts. Shared rasterization helpers are acceptable below the submission boundary, but shared implementation machinery must not collapse public ABI domains. + +Treat publication as latest-complete-submission-wins. This keeps the v1 model simple while leaving room for VM/render parallelism without introducing an unbounded queue. + +## Pitfalls + +Do not preserve immediate rendering through trait-shaped abstractions that still require `&mut Gfx`, `&mut Hardware`, or live VM state during rasterization. That keeps the old coupling under a new name. + +Do not move `composer` responsibilities into `gfx2d`. `composer` owns high-level Game 2D composition. `gfx2d` owns Game 2D primitives. `gfxui` owns Shell UI primitives. Similar local drawing operations below the surface adapter are implementation details. + +Do not keep inherited framebuffer features, such as fade fields or host debug overlays, just because they existed before the boundary. Once the canonical render contract moves to typed submissions, legacy visual behavior either belongs in `RenderManager`, in a proper product UI, or nowhere. + +## Takeaways + +- Rendering architecture should name the producer boundary before introducing backend variety. +- A closed submission is the useful concurrency primitive, even when v1 consumes it synchronously. +- App mode is part of the render contract because it determines which ABI domains and packets are valid. +- Shared rendering code below the adapter is fine; shared public command shapes across unrelated domains are usually a design leak. diff --git a/discussion/workflow/agendas/AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md b/discussion/workflow/agendas/AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md deleted file mode 100644 index 69feada6..00000000 --- a/discussion/workflow/agendas/AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md +++ /dev/null @@ -1,446 +0,0 @@ ---- -id: AGD-0038 -ticket: render-frame-packet-boundary -title: Logical Render Pipelines and Command Packets -status: accepted -created: 2026-05-25 -resolved: -decision: -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -# Agenda - Logical Render Pipelines and Command Packets - -## Contexto - -Depois da migracao para RGBA8888, o proximo passo arquitetural do renderer nao deve ser GPU, 3D, multiplos backends ou troca dinamica complexa de pipeline. O objetivo imediato e separar melhor tres responsabilidades: - -1. pipelines logicos produzem comandos de alto nivel; -2. uma implementacao de hardware/render surface consome esses comandos; -3. o framebuffer RGBA8888 atual continua sendo a superficie final no desktop. - -A direcao desejada agora e explicitar pelo menos dois pipelines logicos: - -```text -Game 2D Pipeline - scene + sprites + HUD - -> Game2DFramePacket / Game2DCommandSet - -> hardware/render implementation - -> framebuffer RGBA8888 - -Prometeu UI Pipeline - shell/home/system UI/windows - -> UiFramePacket / UiCommandSet - -> hardware/render implementation - -> framebuffer RGBA8888 -``` - -Esses pipelines podem funcionar de formas totalmente diferentes. O ponto comum nao deve ser uma API unica pobre que force tudo a parecer igual. O ponto comum deve ser a fronteira operacional: cada pipeline publica seu proprio conjunto logico de comandos para uma implementacao de hardware/superficie. - -Com isso, se depois surgir um pipeline 3D, outro pipeline 2D, ou um renderer especializado para hardware barato/DIY, o trabalho fica concentrado no novo pipeline e/ou no consumidor de comandos, sem reabrir a VM inteira. - -## Problema - -Hoje o runtime ainda mistura producao logica de frame com desenho imediato no `Gfx`/framebuffer. - -O codigo ja tem separacoes parciais: - -- `FrameComposer` coordena scene binding, camera, cache/resolver e sprites do jogo. -- `Gfx` rasteriza em `back`, faz `present()` para `front`, e expoe `front_buffer()` para o host. -- `VirtualMachineRuntime` chama `hw.render_frame()` quando o frame logico termina. -- Prometeu Hub, splash/crash e varias chamadas `gfx.*` ainda desenham diretamente via `gfx_mut()`. -- O host desktop copia o `front_buffer()` RGBA8888 para `pixels`. - -O limite atual ainda e "codigo logico chama renderer", nao "pipeline publica comandos para uma implementacao". - -Isso cria custo futuro: - -- o pipeline 2D de jogo fica preso ao formato operacional atual de `Gfx`; -- a UI do Prometeu fica misturada ao mesmo caminho imperativo de primitives; -- um futuro pipeline 3D teria que disputar o mesmo contrato em vez de definir sua propria saida logica; -- hardware barato/DIY teria que emular detalhes de `Gfx` em vez de consumir um command set adequado; -- testes ficam presos a pixels finais mesmo quando o que interessa e o pacote logico produzido. - -## Pontos Criticos - -### 1. Pipeline nao e backend - -Pipeline deve significar "modelo logico de producao de comandos". Backend/implementacao deve significar "como esses comandos viram pixels/superficie". - -Nao queremos agora: - -- registry sofisticado de backends; -- troca dinamica de pipeline; -- GPU backend; -- 3D; -- refatoracao agressiva de asset pipeline; -- mudanca visual. - -Queremos uma fronteira para que esses itens possam existir depois sem contaminar o passo atual. - -### 2. Game 2D e Prometeu UI nao precisam compartilhar o mesmo command set - -O pipeline 2D de jogos naturalmente fala de scene/cache/camera/sprites/HUD. - -O pipeline de UI do Prometeu naturalmente fala de janelas, paineis, texto, retangulos, estados de foco, home/shell/system UI, talvez layout e clipping. - -Forcar ambos a um `RenderCommand` generico demais no v1 pode criar uma abstracao fraca. A agenda deve decidir se o contrato comum sera: - -- um enum top-level de pacotes por pipeline; ou -- uma trait de submissao ao hardware; ou -- um `FrameSubmission` que contem command sets tipados por dominio. - -### 3. `FrameComposer` continua sendo bom candidato para o Game 2D Pipeline - -`FrameComposer` ja e o ponto natural para produzir o pacote de jogo: - -- scene binding; -- camera; -- cache/resolver; -- sprites por frame; -- HUD/primitives de jogo quando aplicavel. - -O menor corte para jogo ainda parece ser transformar `FrameComposer.render_frame(&mut dyn GfxBridge)` em uma producao de `Game2DFramePacket`, consumida em seguida pelo renderer Classic2D atual. - -### 4. Prometeu UI precisa sair do desenho direto, mas talvez nao no mesmo PR - -Prometeu Hub e system UI hoje usam `hw.gfx_mut()` diretamente. Isso e exatamente o tipo de acoplamento que a arquitetura quer remover, mas migrar UI junto com Game 2D pode deixar a primeira mudanca grande demais. - -A decisao precisa escolher entre: - -- definir os dois contratos agora e implementar em fases; -- implementar primeiro Game 2D e registrar UI como segundo plan; -- ou implementar um envelope comum e dois producers minimos no mesmo plano. - -### 5. HUD pertence ao pipeline 2D de jogo, nao ao Prometeu UI por padrao - -Para esta agenda, "HUD" significa HUD do jogo como parte do frame de jogo: overlays do cart/gameplay, texto/debug de jogo e primitives 2D associadas ao jogo. - -Prometeu UI significa UI do sistema: hub, shell, janelas, home, crash/splash se forem tratados como telas do sistema. - -Essa separacao evita confundir UI de jogo com UI do console. - -### 6. A implementacao inicial ainda pode ser uma so - -Mesmo com dois pipelines logicos, a implementacao inicial pode continuar sendo o `Gfx`/Classic2D atual gravando em RGBA8888. O ganho arquitetural nao exige multiplos backends. Exige que a entrada da implementacao deixe de ser acesso direto ao framebuffer e passe a ser submissao de comandos logicos. - -### 7. A fronteira deve preparar VM e render pipeline paralelos - -Render thread nao e objetivo do v1, mas a fronteira nao deve impedir que a VM e o renderer rodem em paralelo depois. - -Isso implica que a submissao de frame deve ser tratada como um snapshot fechado do trabalho de render daquele frame. Depois de publicada, a VM nao deve continuar mutando estruturas que o renderer precisa ler. A implementacao inicial pode consumir o snapshot imediatamente e no mesmo thread, mas o contrato deve evitar dependencias como `&mut Hardware`, `&mut Gfx`, ou acesso mutavel ao estado vivo da VM durante a rasterizacao. - -Para o v1, isso sugere: - -- `RenderSubmission` deve carregar `frame_id`, `app_mode` e dados suficientes para identificar a ordem de publicacao; -- command sets pequenos devem preferir dados owned ou buffers owned pelo produtor ate a publicacao; -- referencias pesadas, como cache/assets, devem ser read-only e ter ownership compartilhado ou lifetime claramente limitado; -- a camada de hardware/render surface deve ser a unica dona de publicacao/present; -- a VM deve produzir/submeter comandos, nao desenhar nem esperar acesso ao framebuffer; -- testes devem conseguir validar o packet/submission sem depender apenas do framebuffer final. - -### 8. Deve existir um `RenderManager` acima dos pipelines - -Mesmo evitando registry sofisticado de backends no v1, a arquitetura precisa de uma camada acima dos pipelines individuais. Essa camada nao deve ser "mais um pipeline"; ela deve coordenar: - -- qual `AppMode` esta ativo (`Game` ou `Shell`); -- quais command buffers/submissions estao validos para esse modo; -- transicoes entre modos; -- publicacao da superficie; -- politica de latest frame; -- capacidades disponiveis por modo. - -Motivacao principal: transicoes entre Shell e Game nao pertencem completamente a nenhum dos dois pipelines. Exemplo: o Shell UI pode executar uma transicao visual de saida ao abrir um jogo, mas depois que o Game 2D assume, o Shell UI ja nao existe mais para executar a transicao de entrada do jogo. Isso sugere uma base comum de render/control plane acima dos pipelines, capaz de manter transicoes e estado de superficie durante a troca de modo. - -Essa camada deve se chamar `RenderManager`. O nome indica coordenacao de render, nao implementacao de desenho. A decisao precisa separar com cuidado: - -- `RenderManager` como abstracao/runtime contract de coordenacao; -- host/render-surface implementation como adaptador local que sabe publicar em uma superficie real. - -O `RenderManager` coordena pipelines e superficie, mas nao produz comandos de jogo/UI e nao deve conter detalhes de host desktop. - -### 9. Command buffers e submissions sao conceitos diferentes - -Cada dominio/pipeline acumula estado e comandos em seu proprio buffer mutavel durante o frame logico. Exemplos: - -- `ComposerBuffer` para scene, camera e sprites; -- `Gfx2dBuffer` para primitivas 2D de jogo; -- `GfxUiBuffer` para primitivas de Shell UI; -- `ComposerBuffer` tambem cobre HUD do Game 2D no v1. - -No fechamento do frame, o `RenderManager` transforma esses buffers na `RenderSubmission` valida para o `AppMode` atual. - -Depois do fechamento, a submission e um snapshot fechado. Produtores logicos nao podem continuar mutando a submission fechada. Essa regra deve ser cravada agora porque e a base para VM e render pipeline rodarem em paralelo no futuro. - -Politica de backlog: nao existe fila infinita. A regra canonica e `latest complete submission wins`. - -## Opcoes - -### Opcao A - Dois command sets tipados, uma implementacao Classic2D inicial - -**Abordagem:** - -- Criar `Game2DFramePacket` para scene/sprites/HUD. -- Criar `UiFramePacket` para Prometeu UI/system UI. -- Criar um envelope simples, por exemplo `RenderSubmission`, que identifica qual pacote esta sendo submetido. -- Fazer a implementacao Classic2D atual consumir os pacotes e continuar escrevendo no framebuffer RGBA8888. -- Implementar em fases: primeiro Game 2D, depois Prometeu UI, ou ambos com producers minimos se o plano ficar pequeno o suficiente. - -**Pro:** - -- preserva modelos logicos diferentes para jogo e UI; -- evita uma command list generica demais; -- prepara 3D ou outros pipelines sem forcar eles a parecerem com 2D; -- deixa claro que pipeline e backend sao coisas diferentes. - -**Con:** - -- cria mais tipos no inicio; -- precisa definir como o hardware recebe pacotes heterogeneos; -- se so Game 2D for migrado no primeiro plan, UI continua acoplada por um tempo. - -**Maintainability:** - -Alta. Esta opcao combina com a direcao de longo prazo sem exigir varios backends agora. - -### Opcao B - Um `RenderCommand` universal para tudo - -**Abordagem:** - -- Criar uma display list unica com comandos como `Clear`, `DrawSprite`, `DrawText`, `FillRect`, `SceneCache`, `Window`, `Transition`. -- Fazer Game 2D e Prometeu UI emitirem a mesma lista. - -**Pro:** - -- facil de passar por uma unica fila; -- simples para logging/replay inicial; -- reduz o numero de tipos. - -**Con:** - -- tende a virar um denominador comum fraco; -- mistura semantica de jogo, UI de sistema e futuros pipelines; -- pode forcar o pipeline 3D futuro a um contrato 2D inadequado; -- aumenta chance de reabrir a arquitetura quando outro pipeline surgir. - -**Maintainability:** - -Media a baixa como arquitetura principal. Pode ser util internamente dentro de um pipeline, mas nao como contrato unico do runtime. - -### Opcao C - Trait comum de pipeline, sem pacote explicito - -**Abordagem:** - -- Definir algo como `RenderPipeline` com metodo `render(&mut HardwareSurface)`. -- Game 2D e UI implementam a trait, mas continuam desenhando por callbacks imediatos. - -**Pro:** - -- pequena mudanca inicial; -- organiza nomes e ownership; -- reduz um pouco o acoplamento por modulo. - -**Con:** - -- nao cria pacote logico observavel; -- continua misturando producao e execucao; -- nao ajuda tanto render thread, replay, testes de comandos ou hardware alternativo. - -**Maintainability:** - -Insuficiente se virar a decisao final. Pode ser uma API auxiliar, mas nao substitui command packets. - -### Opcao D - Definir todos os pipelines e backend abstraction agora - -**Abordagem:** - -- Criar pipeline registry, backend interfaces, CPU/GPU abstractions, surfaces, scheduling, command queues e talvez render thread. - -**Pro:** - -- parece completo no papel; -- antecipa muitos cenarios futuros. - -**Con:** - -- grande demais para o objetivo atual; -- alto risco de abstrair sem uso real; -- pode atrasar a estabilizacao do 2D atual; -- tende a introduzir nomes e contratos que depois precisarao ser quebrados. - -**Maintainability:** - -Baixa para o momento atual. Melhor deixar essa camada emergir depois de pelo menos dois pipelines reais funcionando. - -## Sugestao / Recomendacao - -A recomendacao atual e a Opcao A: definir dois pipelines logicos com command sets proprios e uma implementacao Classic2D inicial. - -Modelo recomendado: - -```text -Game2DPipeline - produces Game2DFramePacket - owns/uses FrameComposer concepts: - scene/cache/camera - sprites - game HUD - HUD/primitives - -PrometeuUiPipeline - produces UiFramePacket - owns/uses system UI concepts: - home/hub - shell windows - panels/rects/text - focus/input visual state - -RenderHardware / RenderSurface implementation - consumes typed submissions: - Game2D(Game2DFramePacket) - PrometeuUi(UiFramePacket) - future: Game3D(...), Custom2D(...), etc. - writes to current RGBA8888 framebuffer for now - -RenderManager - owns current AppMode render routing - closes per-domain command buffers into a submission - manages mode transitions - keeps only the latest complete submission - owns publish/present policy through the render surface -``` - -O primeiro plano de execucao pode ser incremental: - -1. Definir nomes e contratos dos pacotes sem criar backend registry. -2. Migrar o Game 2D path para `Game2DFramePacket -> Classic2D`. -3. Manter comportamento pixel-level identico. -4. Em seguida migrar Prometeu UI para `UiFramePacket -> Classic2D`. -5. So depois discutir outros backends, render thread ou 3D. - -Se quisermos reduzir ainda mais risco, a decisao pode determinar que o primeiro PR implementa apenas o Game 2D packet, mas ja reserva a arquitetura e nomes para `UiFramePacket`. O importante e nao fingir que Prometeu UI e apenas "mais primitives gfx" dentro do pipeline de jogo. - -## Riscos - -- **Abstracao comum errada:** um command set universal pode parecer simples agora, mas virar bloqueio para UI e 3D depois. -- **Escopo grande demais:** migrar Game 2D, UI, firmware screens e host presentation em um unico PR aumenta risco de regressao visual. -- **Dual path permanente:** se `gfx_mut()` continuar como escape hatch normal, a fronteira de comandos nao vira contrato real. -- **Ownership/lifetime dos pacotes:** pacotes borrowed reduzem copia no v1; pacotes owned ajudam render thread futura. A decisao precisa declarar o alvo do primeiro passo. -- **HUD ambiguo:** HUD de jogo e UI do Prometeu precisam ficar separados por contrato, mesmo que ambos sejam 2D. -- **Present/publication:** `present()` precisa ser tratado como publicacao de superficie, nao como parte aleatoria de cada produtor logico. -- **Asset/cache leakage:** command packets devem referenciar recursos logicos/resolvidos sem copiar asset pipeline nem expor detalhes demais do `Gfx`. -- **Paralelismo futuro:** se o packet v1 depender de estado mutavel vivo da VM/hardware, uma render thread futura exigira nova quebra arquitetural. A submissao precisa parecer um snapshot fechado mesmo quando consumida sincronicamente no v1. -- **Transicoes entre modos:** se Shell e Game forem totalmente independentes sem uma coordenacao mestre, efeitos de transicao e manutencao de superficie ficam sem dono claro. -- **Backpressure:** render futuro nao deve acumular fila infinita. A politica aceita e manter somente a submissao completa mais recente. -- **Abstracao vs host:** `RenderManager` deve ser contrato runtime/abstracao de coordenacao. Host desktop/render surface deve ser implementacao local separada, sem vazar detalhes para os pipelines. -- **HUD ownership:** HUD nao deve ficar escondido dentro de `Gfx2D`. `Gfx2D` deve conter apenas primitivas. No v1, HUD pertence explicitamente ao `composer`. - -## Arquivos / Areas Provavelmente Afetados em uma Plan Futura - -Nucleo Game 2D: - -- `crates/console/prometeu-hal/src/` para tipos compartilhados de packet/submission, se virarem contrato de hardware. -- `crates/console/prometeu-hal/src/lib.rs` para exportar novos modulos. -- `crates/console/prometeu-drivers/src/frame_composer.rs` para produzir `Game2DFramePacket`. -- `crates/console/prometeu-drivers/src/gfx.rs` para consumir `Game2DFramePacket` no Classic2D atual. -- `crates/console/prometeu-drivers/src/hardware.rs` para conectar pipeline/submission/implementacao. -- `crates/console/prometeu-hal/src/hardware_bridge.rs` para reduzir acesso direto a `GfxBridge` no fluxo de frame. - -Nucleo Prometeu UI: - -- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` -- `crates/console/prometeu-system/src/services/windows/*` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs` -- possivelmente um novo modulo de UI commands em `prometeu-hal` ou `prometeu-system`. - -Possivel, mas deve ser minimizado no primeiro passo: - -- `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs` se `gfx.*` primitives forem convertidas em command submission. -- `crates/console/prometeu-hal/src/gfx_bridge.rs` se a ponte antiga for estreitada. -- `crates/host/prometeu-host-desktop-winit/src/runner.rs` se o acesso ao framebuffer for encapsulado, mas host presentation idealmente fica igual no v1. - -## Perguntas em Aberto - -- [x] O primeiro plano deve implementar Game 2D e Prometeu UI juntos, ou apenas Game 2D com contrato preparado para UI? - - Resposta: faseado. O primeiro plano deve priorizar Game 2D, mas o contrato deve nomear Prometeu UI como pipeline separado desde o inicio. -- [x] O contrato comum deve ser um enum `RenderSubmission`, uma trait de submissao, ou uma fila tipada por pipeline? - - Resposta: `AppMode` e `RenderSubmission` devem andar juntos. `AppMode::Game` e `AppMode::Shell` podem ter cadencias e pipelines diferentes; alguns renderers pertencem a um modo e nao ao outro. O envelope inicial pode ser um `RenderSubmission` tipado por modo/pipeline, sem registry dinamico. -- [x] `Game2DFramePacket` deve incluir HUD de jogo no v1 ou apenas reservar o campo? - - Resposta: deve incluir HUD de jogo no v1. O pipeline 2D de jogos abrange scene, sprites e HUD. -- [x] `UiFramePacket` deve representar UI em comandos baixos (`rect`, `text`, `clip`) ou em comandos mais semanticos (`window`, `panel`, `button`)? - - Resposta: comandos de UI podem ser diferentes dos comandos Game 2D, mas widget/layout deve ficar fora da implementacao do host. No host, `UiFramePacket` pode ser reduzido a diretivas semelhantes ou equivalentes as primitivas finais de desenho. -- [x] Pacotes v1 devem ser borrowed, owned, ou hibridos? - - Resposta: hibridos. O modelo precisa suportar transicoes de modo, como clicar no jogo, sair da UI, abrir o jogo e alternar entre `Game` e `Shell`, sem exigir copia pesada de todo recurso. -- [x] Onde fica `present()` no novo modelo: no hardware/render surface, no consumidor Classic2D, ou temporariamente no firmware? - - Resposta: `present()` pertence a implementacao de hardware/render surface quando ela precisa publicar/trocar buffers. Ele faz sentido em um blit/double-buffer path como Game 2D, mas nao deve ser parte obrigatoria do path canonico de todos os pipelines. -- [x] O acesso publico a `gfx.*` deve virar submissao de comandos, ser mantido como debug overlay temporario, ou ser separado em outra discussao? - - Resposta: separar apenas as primitivas de desenho por contrato de pipeline. O que ja e `composer.*` deve continuar no dominio `composer`, porque scene, camera, sprites, HUD e orquestracao de frame sao um contrato mais alto nivel do Game 2D, nao primitivas `gfx2d`. `Gfx2D` deve conter somente primitivas. O atual `gfx.*` deve ser dividido em primitivas apropriadas para Game e Shell, por exemplo `gfx2d.*` para primitivas do Game 2D e `gfxui.*` para Prometeu UI/Shell. As stdlibs podem continuar expondo nomes de alto nivel como `gfx`, desde que apontem para syscalls diferentes por `AppMode`. -- [x] A agenda aberta `DSC-0011` de dirty regions deve depender desta fronteira antes de continuar? - - Resposta: sim. `DSC-0011` vem depois desta fronteira, para nao otimizar o acoplamento errado. -- [x] O v1 precisa implementar render thread? - - Resposta: nao. Mas o v1 deve preparar a fronteira para isso: `RenderSubmission` deve ser um snapshot fechado por `frame_id`/`AppMode`, sem exigir `&mut Hardware` ou framebuffer mutavel no produtor logico. O consumo pode continuar sincrono na primeira implementacao. -- [x] Quem e dono do frame builder? - - Resposta: cada dominio/pipeline deve ter seus proprios buffers de comando/estado. O fechamento do frame deve ser centralizado no host/render coordinator, que transforma esses buffers na `RenderSubmission` valida para o `AppMode` atual. -- [x] Qual e a ordem de composicao Game 2D? - - Resposta: scene e sprites intercalam por `layer` inteiro usando os 4 layers disponiveis do Game 2D, normalmente como `sprite -> scene -> sprite -> scene -> ...` conforme ordenacao por layer. Depois vem um layer de HUD sobre tudo e entao publish. Transicoes visuais devem pertencer ao `RenderManager`, nao ao Game 2D frame path. -- [x] Como lidar com transicoes Game/Shell? - - Resposta: a arquitetura precisa de um `RenderManager` acima dos pipelines. Ele gerencia troca de modo, manutencao da superficie, capacidades ativas e transicoes que nao pertencem integralmente nem ao Shell UI nem ao Game 2D. -- [x] Qual politica de backpressure para render paralelo futuro? - - Resposta: sem fila infinita. Manter somente a submissao completa mais recente. -- [x] Capabilities devem ser separadas? - - Resposta: sim. O contrato deve prever capabilities separadas para `composer`, `gfx2d` e `gfxui`, reforcando o isolamento por `AppMode`. -- [x] O host debug overlay deve continuar? - - Resposta: nao como parte desta arquitetura. O overlay atual pode ser removido por completo em uma execucao futura; se um console/debug UI voltar a ser necessario, deve ser rediscutido como produto proprio, nao como overlay host-owned herdado. -- [x] Fade deve permanecer em algum contrato? - - Resposta: nao. Fade deve sair completamente. Ele era uma ideia de hardware inicial e nao deve permanecer como campo de packet, syscall ou habito de implementacao. Transicoes visuais futuras pertencem ao `RenderManager`, nao a `composer`, `gfx2d` ou `gfxui`. -- [x] HUD deve ser dominio proprio ou parte do `composer`? - - Resposta: HUD fica dentro de `composer` no v1, por ser parte alta do frame Game 2D junto de scene, camera e sprites. `Gfx2D` continua limitado a primitivas. - -## Criterio para Encerrar - -Esta agenda pode virar decisao quando houver consenso sobre: - -- quais pipelines existem no v1; -- qual command set pertence ao Game 2D; -- qual command set pertence a Prometeu UI; -- qual e a fronteira comum entre pipeline e implementacao de hardware/superficie; -- o que fica explicitamente fora de escopo; -- estrategia de migracao incremental sem mudanca visual; -- testes de equivalencia esperados. - -## Discussion - -Atualizacao de 2026-05-25: - -O foco nao deve ser apenas "introduzir `RenderFramePacket` para Classic2D". O foco deve ser separar pipelines logicos por dominio. Game 2D e Prometeu UI podem ter modelos de comando distintos, mas ambos devem submeter comandos a uma implementacao de hardware/render surface. - -A menor refatoracao segura provavelmente continua partindo do Game 2D, porque `FrameComposer` ja concentra scene/cache/sprites. Mas a decisao deve nomear Prometeu UI como pipeline separado desde agora, para evitar que a UI do sistema vire uma extensao acidental do pipeline de jogo. - -Respostas fechadas em discussao: - -- `AppMode::Game` e `AppMode::Shell` continuam sendo os modos canonicos atuais; nao ha rename para `System`. -- `RenderSubmission` deve ser coerente com `AppMode`, porque a atualizacao de frame e os renderers disponiveis podem diferir entre Game e Shell. -- `composer.*` deve continuar representando scene, camera, sprites, HUD e orquestracao de frame do Game 2D. -- `Gfx2D` deve representar apenas primitivas/drawing commands do pipeline Game 2D. -- `GfxUI` deve representar primitivas/drawing commands do pipeline Prometeu UI/Shell. -- As stdlibs PBS de Game e Shell podem usar nomes parecidos em alto nivel, inclusive `gfx`, mas devem mapear para dominios ABI diferentes conforme `AppMode`. -- O host/render implementation pode reduzir command sets diferentes a diretivas locais parecidas, mas widget/layout nao deve morar no host. -- `present()` e publicacao de superficie, nao parte obrigatoria de todo pipeline logico. -- `DSC-0011` deve aguardar esta fronteira. -- Render thread futura nao deve ser implementada agora, mas a fronteira deve nascer compativel com VM e renderer rodando em paralelo: submissions fechadas, leitura imutavel de recursos compartilhados e publicacao isolada na render surface. -- Buffers de comando/estado devem pertencer aos dominios/pipelines; o fechamento em `RenderSubmission` deve ser centralizado no `RenderManager`. -- `RenderManager` e abstracao/runtime contract; host/render-surface implementation e adaptador local separado. -- Game 2D intercalara scene e sprites por layer. HUD fica acima de tudo e pertence ao `composer` no v1, ainda com contrato minimo e evolutivo. -- Fade sai completamente. Transicoes de entrada/saida entre Shell e Game pertencem ao `RenderManager`. -- Backpressure futuro: latest complete submission wins. -- Capabilities devem ser separadas para `composer`, `gfx2d` e `gfxui`. -- O debug overlay host-owned atual nao deve ser preservado como requisito; pode ser removido quando a execucao chegar nessa area. - -## Resolution - -As perguntas principais de escopo foram respondidas. A agenda ainda precisa ser aceita explicitamente antes de virar decisao normativa. - -## Next Step - -Fechar as perguntas sobre escopo do v1. Se a direcao de dois pipelines logicos for aceita, transformar esta agenda em uma decisao normativa antes de qualquer plan ou alteracao de codigo. diff --git a/discussion/workflow/decisions/DEC-0030-logical-render-pipeline-command-boundary.md b/discussion/workflow/decisions/DEC-0030-logical-render-pipeline-command-boundary.md deleted file mode 100644 index 49683c13..00000000 --- a/discussion/workflow/decisions/DEC-0030-logical-render-pipeline-command-boundary.md +++ /dev/null @@ -1,236 +0,0 @@ ---- -id: DEC-0030 -ticket: render-frame-packet-boundary -title: Logical Render Pipeline Command Boundary -status: accepted -created: 2026-05-25 -ref_agenda: AGD-0038 -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -# Decision - Logical Render Pipeline Command Boundary - -## Status - -Accepted. - -## Contexto - -Prometeu ja concluiu a migracao do framebuffer/runtime para RGBA8888. O proximo passo do renderer nao e introduzir GPU, 3D, multiplos backends, troca dinamica complexa de pipeline ou render thread. O objetivo imediato e criar uma fronteira arquitetural limpa entre: - -- producao logica de comandos de render; -- fechamento/publicacao de frame; -- coordenacao entre `AppMode::Game` e `AppMode::Shell`; -- rasterizacao/publicacao em uma superficie real. - -O estado atual ainda acopla codigo logico ao `Gfx`/framebuffer. `FrameComposer` ja centraliza parte da orquestracao do frame de jogo, mas ainda desenha por `GfxBridge`. Prometeu Hub, shell/system UI, splash/crash e primitivas `gfx.*` tambem acessam desenho imperativo direto. Essa decisao normatiza a nova fronteira de comandos e o papel de cada dominio. - -## Decisao - -Prometeu MUST separar render pipeline logico de render implementation. - -O runtime SHALL introduce a `RenderManager` as the runtime-level render coordination abstraction. `RenderManager` MUST NOT be a host-specific renderer and MUST NOT contain desktop presentation details. Host/render-surface implementations SHALL adapt `RenderManager` submissions to the local hardware or host surface. - -The canonical render flow SHALL be: - -```text -domain buffers during logical frame --> RenderManager closes buffers --> RenderSubmission snapshot --> render surface / implementation consumes submission --> RGBA8888 surface publication -``` - -The v1 logical render domains are: - -- `composer.*` for Game 2D high-level frame composition: scene, camera, sprites, HUD, and Game 2D frame orchestration. -- `gfx2d.*` for Game 2D primitives only. -- `gfxui.*` for Shell UI primitives. - -The v1 app modes are: - -- `AppMode::Game`, which SHALL route to Game 2D submissions. -- `AppMode::Shell`, which SHALL route to Shell UI submissions. - -No `AppMode::System` rename is accepted by this decision. - -`RenderSubmission` MUST be coherent with `AppMode`. The initial submission model SHOULD be a typed envelope rather than a generic universal command list: - -```text -RenderSubmission -- frame_id -- app_mode -- packet: - - Game2D(Game2DFramePacket) - - ShellUi(UiFramePacket) -``` - -The exact Rust layout may evolve during planning, but the contract MUST preserve typed per-pipeline command sets. - -## Rationale - -Typed command sets preserve domain meaning. Game 2D and Shell UI have different responsibilities and should not be forced into a weak universal command list. A future 3D or custom 2D pipeline should add its own packet rather than reinterpret a 2D/UI command model. - -`RenderManager` is required because mode transitions do not belong completely to either Shell UI or Game 2D. Shell can render an exit transition before launching a game, but once Game mode owns the frame, Shell UI is no longer available to render the entry transition. Therefore transition coordination, surface maintenance, active render capabilities, and frame publication need a manager above individual pipelines. - -Command buffers and submissions must be distinct. Domain buffers are mutable while the logical frame is being built. A `RenderSubmission` is a closed snapshot. This makes future VM/render parallelism possible without requiring a render thread in v1. - -The current fade model is rejected. Fade was a hardware-direction idea that no longer belongs in the Prometeu render contract after RGBA8888. Fade MUST NOT remain as a packet field, syscall, or implementation habit. Future visual transitions belong to `RenderManager`. - -## Invariantes / Contrato - -### Pipeline and Implementation Boundary - -- Pipelines MUST produce logical commands or state. -- Render implementations MUST consume submissions and write/publish to a surface. -- Pipeline code MUST NOT require direct mutable access to the framebuffer. -- Pipeline code MUST NOT depend on host-specific presentation details. - -### RenderManager - -- `RenderManager` MUST coordinate active `AppMode`, submission closure, mode transitions, capabilities, and publication policy. -- `RenderManager` MUST be a runtime abstraction, not a host renderer. -- Host/render-surface implementations MUST be separate adapters. -- `RenderManager` MUST own transition coordination between Game and Shell. -- `RenderManager` MUST own the policy for surface publication/present through the render surface. - -### Command Buffers and Submissions - -- Each render domain/pipeline MUST own its own mutable command/state buffer during a logical frame. -- `RenderManager` MUST close the active buffers into a `RenderSubmission` at frame boundary. -- Once closed, `RenderSubmission` MUST be treated as immutable by producers. -- `RenderSubmission` MUST include `frame_id` and `app_mode`. -- Future parallel render MUST be supported by the contract: consuming a submission MUST NOT require `&mut Hardware`, `&mut Gfx`, or live mutable VM state. -- Backpressure policy MUST be latest-complete-submission-wins. The system MUST NOT accumulate an unbounded frame queue. - -### AppMode Routing - -- `AppMode::Game` SHALL expose and route Game render domains. -- `AppMode::Shell` SHALL expose and route Shell UI render domains. -- Renderers/capabilities MAY differ by app mode. -- PBS stdlibs MAY expose similar high-level names, including `gfx`, but they MUST map to mode-appropriate ABI domains. - -### ABI Domains - -- `composer.*` MUST remain the high-level Game 2D composition domain. -- `composer.*` MUST own scene, camera, sprites, HUD, and Game 2D frame orchestration. -- `gfx2d.*` MUST contain Game 2D primitives only. -- `gfx2d.*` MUST NOT own scene, camera, sprites, HUD, or frame orchestration. -- `gfxui.*` MUST contain Shell UI primitives. -- `gfxui.*` MUST NOT contain widget/layout policy. Widget/layout belongs in Shell/UI code or stdlib, not the host implementation. -- Capabilities MUST be separated for `composer`, `gfx2d`, and `gfxui`. - -### Game 2D Composition - -- Game 2D scene and sprites MUST compose by integer `layer`. -- The v1 Game 2D model has four available layers. -- Scene and sprite composition MAY interleave by layer, normally as sprite/scene ordering across the four layers. -- HUD MUST render above scene/sprite composition. -- HUD belongs to `composer` in v1 and remains a minimal, evolvable contract. -- HUD in this decision means Game HUD only. Shell/system UI is always part of the Shell UI pipeline. -- Game 2D publish happens after scene/sprite/HUD composition. - -### Shell UI Composition - -- Shell UI MUST use its own `gfxui.*` primitive command set. -- Shell UI commands MAY reduce to the same local drawing directives as Game 2D primitives inside a host/render-surface implementation. -- That reduction MUST remain an implementation detail and MUST NOT collapse the ABI domains. - -### Fade and Transitions - -- Fade MUST be removed completely from canonical pipeline contracts. -- Fade MUST NOT appear as a `Game2DFramePacket`, `UiFramePacket`, `RenderSubmission`, `composer`, `gfx2d`, or `gfxui` field or syscall. -- Future visual transitions MUST be coordinated by `RenderManager`. - -### Debug Overlay - -- The current host-owned debug overlay is not part of the new render architecture. -- It MAY be removed during execution. -- A future console/debug UI MUST be discussed as a separate product/domain, not preserved as inherited host overlay behavior. - -## Impactos - -### Spec - -Specs must define the logical render pipeline model, `RenderManager`, `RenderSubmission`, app-mode routing, domain buffers, and publication boundary in English. - -The public syscall/ABI specs must replace the old primitive `gfx.*` surface with mode-appropriate `gfx2d.*` and `gfxui.*` primitive domains while preserving `composer.*` as the high-level Game 2D composition domain. - -### Runtime - -Runtime frame execution must stop treating `Gfx`/framebuffer as the direct target of logical render calls. It must accumulate domain state/commands and let `RenderManager` close submissions. - -`VirtualMachineRuntime` and firmware flow must route frame closure through `RenderManager`. - -### HAL - -HAL must expose distinct contracts for: - -- `composer`; -- `gfx2d`; -- `gfxui`; -- render submissions; -- render manager/surface boundary. - -Capability flags must be split accordingly. - -### Host - -The desktop host remains a concrete render-surface implementation. It may continue publishing RGBA8888 to `pixels`, but that behavior must sit below the render-surface adapter, not inside logical pipeline APIs. - -Host debug overlay may be removed. - -### Firmware / Shell - -Shell UI must migrate away from direct `gfx_mut()` drawing toward `gfxui.*`/Shell UI command buffers. - -Game mode must migrate toward `composer.*` plus `gfx2d.*` buffers closed by `RenderManager`. - -### PBS / Stdlibs - -PBS Game and Shell stdlibs may present similar high-level APIs to authors. They must map to different ABI domains according to `AppMode`. - -Game stdlib should map scene/camera/sprites/HUD to `composer.*` and primitive Game drawing to `gfx2d.*`. - -Shell stdlib should map UI primitives to `gfxui.*`. - -## Alternativas Descartadas - -### Universal RenderCommand - -A single `RenderCommand` display list for Game, Shell, and future pipelines is rejected. It collapses domain meaning and would likely force future 3D or custom pipelines into a 2D/UI-shaped contract. - -### Trait-Only RenderPipeline - -A trait that calls `render(&mut Surface)` without a closed packet is rejected as the primary boundary. It preserves immediate rendering and does not prepare VM/render parallelism. - -### Backend Registry Now - -A full backend registry, GPU/CPU backend abstraction, dynamic pipeline switching, render thread, or 3D implementation is out of scope. Those may come later after the command boundary is stable. - -### Moving Composer into Gfx2D - -Moving scene, camera, sprites, HUD, or frame orchestration into `gfx2d.*` is rejected. `gfx2d.*` is primitives only. - -## Referencias - -- Agenda: `AGD-0038` -- Related dependency: `DSC-0011` dirty regions must wait until this boundary exists. -- Related prior lessons: frame composition belongs above the render backend; public ABI must follow canonical service boundaries; RGBA8888 is the canonical framebuffer contract. - -## Propagacao Necessaria - -- Create a plan before any spec or code changes. -- Update canonical specs in English. -- Update HAL syscall registry, capability definitions, and resolver tests. -- Update PBS/stdlib syscall declarations for Game and Shell. -- Update runtime dispatch so render syscalls mutate domain buffers instead of drawing directly. -- Introduce `RenderManager`, domain buffers, `RenderSubmission`, and render-surface implementation boundary. -- Migrate Game 2D first, then Shell UI, unless the plan proves both can be safely migrated together. -- Remove fade surfaces from render contracts. -- Remove or explicitly retire the host debug overlay. -- Add packet/submission tests, app-mode capability tests, and pixel-equivalence tests for unchanged visible behavior. - -## Revision Log - -- 2026-05-25: Initial decision draft from `AGD-0038`. diff --git a/discussion/workflow/plans/PLN-0073-render-contract-specs.md b/discussion/workflow/plans/PLN-0073-render-contract-specs.md deleted file mode 100644 index e71859dc..00000000 --- a/discussion/workflow/plans/PLN-0073-render-contract-specs.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -id: PLN-0073 -ticket: render-frame-packet-boundary -title: Render Contract Specs -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Publish canonical English spec language for the logical render pipeline boundary before code migration. The specs must make `RenderManager`, domain buffers, typed `RenderSubmission`, app-mode routing, render-surface publication, latest-complete-submission policy, and fade removal explicit. - -## Target - -Canonical specs describe the DEC-0030 render contract well enough that HAL, runtime, host, firmware, and PBS work can proceed without reopening architecture. - -## Scope - -- Define the logical flow from mutable domain buffers to closed `RenderSubmission` to render-surface publication. -- Define `RenderManager` as runtime coordination, not a host renderer. -- Define `RenderSubmission` with `frame_id`, `app_mode`, and typed Game 2D or Shell UI packet. -- Define `composer.*`, `gfx2d.*`, and `gfxui.*` as separate ABI domains. -- Specify latest-complete-submission-wins backpressure. -- Remove fade from canonical render contracts. - -## Out of Scope - -- Rust implementation. -- PBS stdlib implementation. -- Pixel output changes. -- Backend registry, GPU backend, render thread, 3D pipeline, or debug console design. - -## Execution Sequence - -1. Inventory existing render, graphics, framebuffer, composer, and syscall specs. -2. Replace old primitive `gfx.*` ABI language with the DEC-0030 domain split. -3. Add sections for `RenderManager`, domain buffers, submission closure, immutability, and publication. -4. Add app-mode and capability routing language for Game and Shell. -5. Remove normative fade fields, syscalls, and packet references from render specs. -6. Cross-link impacted specs for HAL, runtime, host, firmware, and PBS propagation. - -## Acceptance Criteria - -- Specs are in English and reference `DEC-0030`. -- No canonical render spec presents direct framebuffer mutation as the logical render API. -- No canonical render spec keeps fade as a packet field, syscall, or ABI habit. -- Specs distinguish logical pipeline production from render-surface implementation. - -## Tests / Validation - -- Run repository documentation/spec validation if available. -- Inspect `rg -n "fade|gfx\\.\\*|RenderSubmission|RenderManager|present\\(" specs discussion/workflow/decisions/DEC-0030-logical-render-pipeline-command-boundary.md`. -- Run `discussion validate` if discussion references change. - -## Risks - -- Existing specs may use `gfx.*` as both author API and ABI name; preserve author clarity while changing ABI names. -- Removing fade text can accidentally delete transition requirements; move those requirements to `RenderManager` language. - -## Affected Artifacts - -- `specs/` -- `discussion/workflow/decisions/DEC-0030-logical-render-pipeline-command-boundary.md` diff --git a/discussion/workflow/plans/PLN-0074-hal-render-submission-types.md b/discussion/workflow/plans/PLN-0074-hal-render-submission-types.md deleted file mode 100644 index d405e426..00000000 --- a/discussion/workflow/plans/PLN-0074-hal-render-submission-types.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -id: PLN-0074 -ticket: render-frame-packet-boundary -title: HAL Render Submission Types -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Introduce HAL-level submission types that encode the logical render boundary without binding consumers to live `Hardware`, `Gfx`, framebuffer, or VM state. - -## Target - -HAL exposes typed contracts for `RenderSubmission`, `Game2DFramePacket`, `ShellUiFramePacket`, frame ids, app-mode routing, and immutable closed submissions. - -## Scope - -- Add typed packet structures for Game 2D and Shell UI. -- Add a `RenderSubmission` envelope with `frame_id`, `app_mode`, and typed packet variant. -- Add or reuse a monotonic frame id type. -- Encode closed-submission immutability through ownership and API shape. -- Keep packet types free of host presentation details and live mutable runtime references. - -## Out of Scope - -- `RenderManager` behavior beyond type integration. -- Renderer/rasterizer implementation. -- Backend registry, render thread, GPU path, or 3D packet. -- PBS stdlib updates. - -## Execution Sequence - -1. Locate current HAL graphics, composer, capability, and app-mode modules. -2. Add packet and submission types where ABI-facing render data belongs. -3. Represent packets as typed variants: `Game2D(Game2DFramePacket)` and `ShellUi(ShellUiFramePacket)`. -4. Ensure constructors or closure APIs consume or clone domain buffer contents. -5. Add tests for app-mode and packet coherence. -6. Update internal references to use the new types without changing runtime rendering yet. - -## Acceptance Criteria - -- HAL contains the DEC-0030 submission and packet vocabulary. -- A closed `RenderSubmission` can be handed to a consumer without borrowing live runtime rendering state. -- Game and Shell packets are distinguishable at the type level. -- The HAL API does not include fade fields. - -## Tests / Validation - -- Run HAL crate unit tests. -- Add tests for packet construction, app-mode coherence, and immutable ownership semantics. -- Inspect `rg -n "fade|UiFramePacket|ShellUiFramePacket|Game2DFramePacket|RenderSubmission" crates specs`. - -## Risks - -- New types must live at the ABI/contract level, not inside a host renderer. -- Borrowing shortcuts could leak mutability and violate the future parallel render contract. - -## Affected Artifacts - -- HAL crate/module under `crates/` -- HAL tests diff --git a/discussion/workflow/plans/PLN-0075-rendermanager-core.md b/discussion/workflow/plans/PLN-0075-rendermanager-core.md deleted file mode 100644 index c5dba1a6..00000000 --- a/discussion/workflow/plans/PLN-0075-rendermanager-core.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -id: PLN-0075 -ticket: render-frame-packet-boundary -title: RenderManager Core -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Add `RenderManager` as the runtime abstraction that closes domain buffers into typed submissions, tracks active app mode, owns transition coordination placeholders, and hands the latest complete submission to the render surface. - -## Target - -Runtime frame execution routes render closure through `RenderManager` without making it a host renderer or direct framebuffer writer. - -## Scope - -- Add the `RenderManager` module and public runtime API. -- Track active `AppMode` and frame id generation. -- Close the active domain buffer into a `RenderSubmission`. -- Enforce latest-complete-submission-wins rather than an unbounded queue. -- Add transition coordination placeholders owned by `RenderManager`. -- Provide a publication handoff to a render-surface abstraction. - -## Out of Scope - -- Full visual transition implementation. -- Host-specific presentation details. -- Domain buffer implementation beyond integration points. -- Render thread, GPU backend, or backend registry. - -## Execution Sequence - -1. Add a runtime render module that owns `RenderManager` and related state. -2. Wire `VirtualMachineRuntime` frame closure through `RenderManager`. -3. Add active app-mode routing and rejection for incoherent packet closure. -4. Add latest-complete-submission storage and replacement semantics. -5. Add a render-surface handoff interface without calling host-specific `present()` from producers. -6. Add explicit no-op transition hooks for future work. - -## Acceptance Criteria - -- Runtime has one frame closure authority for render submissions. -- Producers can mutate domain buffers before closure but cannot mutate the closed submission. -- `RenderManager` does not depend on desktop host types. -- Backpressure is latest-complete-submission-wins. -- App-mode routing is explicit and tested. - -## Tests / Validation - -- Add unit tests for frame id increment, latest submission replacement, app-mode routing, and no-op transition state. -- Add runtime integration coverage proving frame closure passes through `RenderManager`. -- Run affected crate tests. - -## Risks - -- Existing frame lifecycle code may assume immediate framebuffer mutation; isolate compatibility shims and remove them in later plans. -- Transition placeholders must not become hidden fade behavior. - -## Affected Artifacts - -- Runtime crate/modules under `crates/` -- Runtime tests diff --git a/discussion/workflow/plans/PLN-0076-capabilities-and-abi-domain-split.md b/discussion/workflow/plans/PLN-0076-capabilities-and-abi-domain-split.md deleted file mode 100644 index b62ce076..00000000 --- a/discussion/workflow/plans/PLN-0076-capabilities-and-abi-domain-split.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -id: PLN-0076 -ticket: render-frame-packet-boundary -title: Capabilities and ABI Domain Split -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Split render capabilities and syscall metadata so `composer`, `gfx2d`, and `gfxui` are independently addressable and enforceable according to `AppMode`. - -## Target - -Capability gates reject wrong-domain render syscalls, and metadata no longer treats the old primitive `gfx.*` ABI as the single render surface. - -## Scope - -- Add or rename capability flags for `COMPOSER`, `GFX2D`, and `GFXUI`. -- Update syscall metadata for all render-domain calls. -- Enforce domain availability by `AppMode::Game` and `AppMode::Shell`. -- Update resolver and capability tests. -- Preserve `composer.*` as Game 2D high-level composition. - -## Out of Scope - -- Implementing command buffers. -- PBS stdlib author-facing wrappers. -- Host rendering. -- Backward compatibility for the old primitive `gfx.*` ABI. - -## Execution Sequence - -1. Locate capability definitions, syscall registry metadata, and resolver tests. -2. Introduce separate render capability identifiers for `composer`, `gfx2d`, and `gfxui`. -3. Update metadata for existing composer calls and new or renamed primitive domains. -4. Add app-mode checks so Game exposes `composer` and `gfx2d`, while Shell exposes `gfxui`. -5. Replace stale tests that assume one graphics capability. -6. Run resolver and runtime syscall tests. - -## Acceptance Criteria - -- Wrong-domain calls fail before mutating render state. -- Game mode cannot use `gfxui.*`. -- Shell mode cannot use `composer.*` or `gfx2d.*`. -- Capability metadata uses the split domain names. -- No compatibility promise remains for the old primitive `gfx.*` ABI. - -## Tests / Validation - -- Add resolver tests for all three capabilities. -- Add app-mode enforcement tests for allowed and rejected calls. -- Run affected unit and integration tests. - -## Risks - -- A large rename can hide semantic mistakes; verify every syscall maps to the correct domain responsibility. -- Author-facing stdlib names may still use `gfx`; keep this plan focused on ABI metadata. - -## Affected Artifacts - -- Syscall metadata and resolver modules under `crates/` -- Capability tests diff --git a/discussion/workflow/plans/PLN-0077-composer-buffer-and-game2d-packet.md b/discussion/workflow/plans/PLN-0077-composer-buffer-and-game2d-packet.md deleted file mode 100644 index 6d9b1c23..00000000 --- a/discussion/workflow/plans/PLN-0077-composer-buffer-and-game2d-packet.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -id: PLN-0077 -ticket: render-frame-packet-boundary -title: Composer Buffer and Game2D Packet -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Convert current `composer.*` frame state into a mutable `ComposerBuffer` that closes into `Game2DFramePacket` at frame boundary. - -## Target - -Game 2D scene, camera, sprites, HUD, and frame orchestration are represented as domain-owned state until `RenderManager` closes them into a typed packet. - -## Scope - -- Add `ComposerBuffer`. -- Move scene, camera, sprite, HUD, layer ordering, and Game 2D orchestration state into the buffer. -- Close the buffer into `Game2DFramePacket`. -- Preserve the v1 four-layer scene/sprite composition model and HUD-above-game rule. -- Ensure fade is absent from Game 2D packet state. - -## Out of Scope - -- Primitive `gfx2d.*` command buffering. -- Shell UI packet work. -- Renderer consumption refactor beyond packet shape. -- New Game 2D features. - -## Execution Sequence - -1. Locate current `FrameComposer`, composer syscalls, scene/camera/sprite/HUD state, and tests. -2. Introduce `ComposerBuffer` and migrate composer mutation paths into it. -3. Implement closure into `Game2DFramePacket` with immutable packet contents. -4. Preserve integer layer ordering across the four Game 2D layers and HUD final composition. -5. Route frame closure through `RenderManager`. -6. Update tests that currently inspect `FrameComposer` internals. - -## Acceptance Criteria - -- `composer.*` owns Game 2D high-level frame state. -- Closed `Game2DFramePacket` contains all data required by the Game 2D renderer without live VM or `Gfx` access. -- Scene/sprite layer behavior and HUD ordering are preserved. -- Fade is not represented. - -## Tests / Validation - -- Add unit tests for buffer mutation and packet closure. -- Add composition ordering tests for scene, sprites, and HUD. -- Run current frame composer/render tests and compare existing expected output. - -## Risks - -- Existing composer code may mix orchestration with drawing; split state capture from rendering before deeper migration. -- Layer behavior is user-visible and must not drift. - -## Affected Artifacts - -- Composer/frame-composer modules under `crates/` -- Game 2D tests diff --git a/discussion/workflow/plans/PLN-0078-classic2d-game-renderer-consumer.md b/discussion/workflow/plans/PLN-0078-classic2d-game-renderer-consumer.md deleted file mode 100644 index 2fd592db..00000000 --- a/discussion/workflow/plans/PLN-0078-classic2d-game-renderer-consumer.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -id: PLN-0078 -ticket: render-frame-packet-boundary -title: Classic2D Game Renderer Consumer -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Refactor the Classic2D renderer path so it consumes `Game2DFramePacket` instead of being called directly by `FrameComposer` or logical producers. - -## Target - -Classic2D remains the concrete Game 2D rasterizer, but its input is a closed packet and its visible pixel output remains equivalent. - -## Scope - -- Add a packet consumer entry point for the current Classic2D/Gfx renderer. -- Replace direct `FrameComposer` to `Gfx` calls with packet consumption. -- Keep RGBA8888 output unchanged for existing supported scenes. -- Keep renderer implementation below the logical pipeline boundary. - -## Out of Scope - -- Redesigning scene, sprite, HUD, or primitive semantics. -- Shell UI rendering. -- Backend registry or GPU work. -- Dirty region optimization. - -## Execution Sequence - -1. Locate the current Classic2D/Gfx rendering entry points and `FrameComposer` call graph. -2. Add a `Game2DFramePacket` consumer API. -3. Move rendering reads from live composer state to packet data. -4. Update frame execution to pass the closed packet into the renderer through the render surface path. -5. Preserve existing framebuffer dimensions, RGBA8888 format, and pixel behavior. -6. Remove direct producer-to-renderer calls that bypass the packet boundary. - -## Acceptance Criteria - -- Classic2D renders from `Game2DFramePacket`. -- Logical composer code no longer directly mutates `Gfx` during frame production. -- Existing Game 2D visual fixtures or tests remain pixel-equivalent except where explicitly updated for the new boundary. - -## Tests / Validation - -- Add packet-to-pixels tests for representative scene, sprite, primitive, and HUD cases. -- Run existing render tests. -- Add regression coverage comparing old expected pixel buffers where available. - -## Risks - -- Packet conversion can accidentally alter draw order; keep tests focused on ordering and final pixels. -- Some current code may depend on side effects from direct `Gfx` mutation; identify and replace those effects explicitly. - -## Affected Artifacts - -- Classic2D/Gfx renderer modules under `crates/` -- Render tests and fixtures diff --git a/discussion/workflow/plans/PLN-0079-gfx2d-primitive-domain.md b/discussion/workflow/plans/PLN-0079-gfx2d-primitive-domain.md deleted file mode 100644 index 3979af28..00000000 --- a/discussion/workflow/plans/PLN-0079-gfx2d-primitive-domain.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -id: PLN-0079 -ticket: render-frame-packet-boundary -title: Gfx2D Primitive Domain -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Replace primitive Game drawing paths with `gfx2d.*` command buffering while keeping `gfx2d` limited to primitives only. - -## Target - -Game primitive drawing becomes a Game-mode ABI domain that records commands into a buffer consumed as part of Game 2D rendering. - -## Scope - -- Add `gfx2d.*` syscall domain and command buffer. -- Migrate primitive Game drawing calls from old `gfx.*` behavior to `gfx2d.*`. -- Ensure `gfx2d` contains primitives only. -- Integrate `gfx2d` commands into the Game 2D packet/render path without taking ownership of scene, camera, sprites, HUD, or frame orchestration. - -## Out of Scope - -- Shell UI primitive domain. -- `composer.*` high-level Game state. -- Widget/layout policy. -- Compatibility with old primitive `gfx.*` ABI names. - -## Execution Sequence - -1. Inventory old primitive `gfx.*` syscalls and direct drawing callsites used by Game mode. -2. Define the `gfx2d.*` command enum and mutable command buffer. -3. Route Game primitive syscalls into the buffer. -4. Add app-mode and capability enforcement for Game-only access. -5. Integrate buffered primitives into Game 2D packet closure or renderer consumption at the DEC-0030-approved layer. -6. Remove or retire old primitive `gfx.*` ABI registrations. - -## Acceptance Criteria - -- Game primitive commands are buffered as `gfx2d.*`. -- `gfx2d` does not own scene, camera, sprites, HUD, or frame orchestration. -- Shell mode cannot call `gfx2d.*`. -- No logical primitive path directly mutates framebuffer during production. - -## Tests / Validation - -- Add syscall tests for each migrated primitive. -- Add command buffer ordering tests. -- Add capability rejection tests for Shell mode. -- Run pixel-equivalence tests for unchanged primitive output. - -## Risks - -- The old `gfx` naming may appear in PBS wrappers; distinguish author API names from ABI domain names. -- Primitive ordering relative to composer output must be explicit. - -## Affected Artifacts - -- Syscall registry and runtime render domain modules under `crates/` -- Game primitive tests diff --git a/discussion/workflow/plans/PLN-0080-shell-ui-packet-and-gfxui-domain.md b/discussion/workflow/plans/PLN-0080-shell-ui-packet-and-gfxui-domain.md deleted file mode 100644 index 87a02b3e..00000000 --- a/discussion/workflow/plans/PLN-0080-shell-ui-packet-and-gfxui-domain.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -id: PLN-0080 -ticket: render-frame-packet-boundary -title: Shell UI Packet and GfxUI Domain -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Add `gfxui.*` command buffering and `ShellUiFramePacket` for Shell UI primitives while keeping widget/layout policy outside the host/render-surface implementation. - -## Target - -Shell UI emits typed UI primitive commands into a Shell-only packet that render-surface implementations can consume without collapsing UI and Game ABI domains. - -## Scope - -- Define `gfxui.*` primitive commands. -- Add a mutable Shell UI command buffer. -- Close Shell UI commands into `ShellUiFramePacket`. -- Enforce Shell-only capability and app-mode access. -- Keep widget/layout policy in Shell/UI code or stdlib layers. - -## Out of Scope - -- Migrating all Shell callsites; that is handled by the Shell/Hub migration plan. -- Game primitive rendering. -- New widget framework. -- Host-owned debug overlay. - -## Execution Sequence - -1. Define the `gfxui.*` syscall and command vocabulary needed for current Shell UI primitives. -2. Add a Shell UI command buffer and closure into `ShellUiFramePacket`. -3. Add capability metadata and resolver enforcement for `GFXUI`. -4. Add a render-surface consumer path that can rasterize Shell UI commands as an implementation detail. -5. Keep layout/widget decisions above the packet boundary. -6. Add tests for closure, app-mode gating, and packet consumption. - -## Acceptance Criteria - -- Shell UI commands close into `ShellUiFramePacket`. -- `gfxui.*` does not contain widget/layout policy. -- Game mode cannot call `gfxui.*`. -- Host/render-surface code consumes commands without redefining Shell UI product policy. - -## Tests / Validation - -- Add Shell UI command buffer unit tests. -- Add app-mode rejection tests for Game mode. -- Add render-surface consumer tests for representative primitives. - -## Risks - -- It is easy to migrate widget policy into host rendering while adding UI primitives; keep commands low-level and policy-free. -- Shell UI and Game primitives may rasterize similarly, but ABI domains must remain distinct. - -## Affected Artifacts - -- Shell UI render domain modules under `crates/` -- Syscall metadata and tests diff --git a/discussion/workflow/plans/PLN-0081-shell-hub-migration.md b/discussion/workflow/plans/PLN-0081-shell-hub-migration.md deleted file mode 100644 index 3ecfaef2..00000000 --- a/discussion/workflow/plans/PLN-0081-shell-hub-migration.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -id: PLN-0081 -ticket: render-frame-packet-boundary -title: Shell Hub Migration -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Move Prometeu Hub, shell windows, splash/crash UI, and Shell drawing away from `gfx_mut()` into `gfxui.*` and Shell UI command buffers. - -## Target - -Shell-facing UI no longer draws directly into `Gfx`; it produces Shell UI commands that close into `ShellUiFramePacket`. - -## Scope - -- Migrate Prometeu Hub UI drawing. -- Migrate shell windows and window-manager drawing. -- Migrate splash and crash UI drawing. -- Remove direct Shell access to `gfx_mut()` for render production. -- Preserve visible Shell output where behavior is not intentionally changed. - -## Out of Scope - -- Designing new Hub visuals. -- New widget/layout framework. -- Game mode migration. -- Debug console replacement. - -## Execution Sequence - -1. Inventory Shell, Hub, splash, crash, and firmware drawing callsites that use `gfx_mut()` or direct framebuffer access. -2. Replace direct primitive drawing with `gfxui.*` buffer writes. -3. Route Shell frame closure through `RenderManager`. -4. Ensure Shell UI packets are consumed by the render-surface implementation. -5. Remove now-unused direct Shell rendering hooks. -6. Update tests and snapshots for unchanged visible behavior. - -## Acceptance Criteria - -- Shell UI production no longer requires mutable `Gfx` access. -- Hub, shell windows, splash, and crash UI can render through `ShellUiFramePacket`. -- Shell mode does not use `composer.*` or `gfx2d.*`. -- Visible UI output is preserved except for approved fixture updates caused by boundary-only refactoring. - -## Tests / Validation - -- Add integration tests for Hub/shell frame production. -- Add tests for splash and crash UI packet output. -- Run host smoke tests where available. -- Use pixel or snapshot comparisons for representative Shell screens. - -## Risks - -- Firmware and Shell code may mix lifecycle side effects with drawing; split command production without changing lifecycle semantics. -- Some host tests may assume immediate framebuffer writes; update them to observe submissions or final pixels. - -## Affected Artifacts - -- Shell, Hub, firmware, and host integration modules under `crates/` -- Shell UI tests and fixtures diff --git a/discussion/workflow/plans/PLN-0082-frame-publication-and-present-boundary.md b/discussion/workflow/plans/PLN-0082-frame-publication-and-present-boundary.md deleted file mode 100644 index 93e5d0bf..00000000 --- a/discussion/workflow/plans/PLN-0082-frame-publication-and-present-boundary.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -id: PLN-0082 -ticket: render-frame-packet-boundary -title: Frame Publication and Present Boundary -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Move `present()` and surface publication behind the render-surface implementation boundary so runtime producers no longer publish buffers directly. - -## Target - -Only the render-surface implementation publishes RGBA8888 output; logical producers and `RenderManager` hand off closed submissions through a boundary. - -## Scope - -- Identify all direct `present()` and surface publication callsites. -- Define the render-surface adapter boundary. -- Route publication through the adapter from `RenderManager`. -- Keep desktop `pixels` publication as a host implementation detail. -- Preserve frame pacing and invalidation behavior. - -## Out of Scope - -- New backend registry. -- GPU implementation. -- Render thread. -- Dirty regions. -- Visual transition design. - -## Execution Sequence - -1. Inventory `present()`, framebuffer publication, and host invalidation callsites. -2. Add or refine a render-surface trait/adapter that consumes `RenderSubmission`. -3. Move host desktop publication below that adapter. -4. Update runtime frame loop to publish only through `RenderManager` and the adapter. -5. Remove direct publication from composer, `gfx2d`, `gfxui`, Shell, and Game producers. -6. Verify frame pacing and host invalidation still occur at the correct boundary. - -## Acceptance Criteria - -- Logical render domains cannot call `present()` or publish buffers directly. -- Host desktop publication remains functional below the render-surface boundary. -- `RenderManager` owns the policy for selecting the latest complete submission. -- Existing frame pacing behavior is not regressed. - -## Tests / Validation - -- Add unit tests for render-surface handoff. -- Add integration tests proving producers cannot publish directly. -- Run host desktop render/pacing tests. -- Inspect `rg -n "present\\(|publish|pixels" crates` for boundary violations. - -## Risks - -- Publication and invalidation may be coupled in current host code; preserve timing semantics while moving ownership. -- A too-broad adapter can become a backend registry prematurely. - -## Affected Artifacts - -- Runtime render manager modules -- Host desktop render surface modules -- Frame pacing tests diff --git a/discussion/workflow/plans/PLN-0083-fade-removal.md b/discussion/workflow/plans/PLN-0083-fade-removal.md deleted file mode 100644 index ce47a208..00000000 --- a/discussion/workflow/plans/PLN-0083-fade-removal.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -id: PLN-0083 -ticket: render-frame-packet-boundary -title: Fade Removal -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Remove fade fields, syscalls, packet members, renderer state, tests, and docs from canonical render contracts. - -## Target - -Fade is no longer a render pipeline feature. Future visual transitions remain the responsibility of `RenderManager` and require separate product/domain work. - -## Scope - -- Remove fade from specs, HAL packet types, syscall metadata, runtime state, renderer state, tests, and PBS declarations. -- Remove or update tests that assert fade behavior. -- Add guard tests or searches that prevent reintroducing fade in canonical render contracts. - -## Out of Scope - -- Implementing replacement visual transitions. -- Replacing fade with another transition API. -- Preserving backward compatibility for fade syscalls. - -## Execution Sequence - -1. Inventory all fade references in specs, crates, tests, examples, and PBS stdlibs. -2. Delete fade fields and syscalls from canonical ABI and packet definitions. -3. Remove renderer/runtime fade state and behavior. -4. Update callers and tests to stop setting or expecting fade. -5. Add documentation language that transitions belong to `RenderManager`. -6. Run targeted and full affected tests. - -## Acceptance Criteria - -- No canonical packet, syscall, or render contract contains fade. -- Runtime and renderer code do not maintain fade state. -- Tests no longer depend on fade behavior. -- Specs clearly state future transitions are `RenderManager` responsibility. - -## Tests / Validation - -- Run `rg -n "\\bfade\\b|Fade" specs crates discussion/workflow` and verify remaining references are only historical decision/agenda text or explicit removal notes. -- Run affected unit and integration tests. -- Add regression tests preventing fade fields in current packet types. - -## Risks - -- Removing tests can reduce coverage; replace fade-specific assertions with packet/transition boundary assertions where appropriate. -- Historical discussion files may keep fade references; do not rewrite history unless the framework requires it. - -## Affected Artifacts - -- Specs -- HAL, runtime, renderer, syscall, PBS, and tests under `crates/` diff --git a/discussion/workflow/plans/PLN-0084-host-debug-overlay-removal.md b/discussion/workflow/plans/PLN-0084-host-debug-overlay-removal.md deleted file mode 100644 index 31607e5a..00000000 --- a/discussion/workflow/plans/PLN-0084-host-debug-overlay-removal.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -id: PLN-0084 -ticket: render-frame-packet-boundary -title: Host Debug Overlay Removal -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Remove the current host-owned debug overlay from the render path. Any future console/debug UI is a separate product/domain discussion. - -## Target - -Host rendering no longer injects an inherited debug overlay into Game or Shell output. - -## Scope - -- Identify host-owned debug overlay code and render hooks. -- Remove overlay rendering from the host render path. -- Remove overlay toggles/tests that assume inherited host drawing. -- Preserve non-render telemetry and logging where they are not tied to overlay drawing. - -## Out of Scope - -- Designing a replacement debug console. -- Moving debug UI into `gfxui.*`. -- Changing telemetry collection. -- Removing unrelated developer diagnostics. - -## Execution Sequence - -1. Inventory debug overlay rendering callsites and tests. -2. Remove host overlay injection from Game and Shell render paths. -3. Delete overlay-specific drawing state, toggles, and fixture expectations. -4. Keep telemetry/logging APIs that do not render overlay pixels. -5. Update docs/specs to state that future debug UI requires a separate decision. -6. Run host and render tests. - -## Acceptance Criteria - -- Host render path does not draw debug overlay pixels. -- Game and Shell output do not depend on host overlay state. -- Remaining diagnostics do not violate the render pipeline boundary. -- Docs do not preserve host overlay as an architectural requirement. - -## Tests / Validation - -- Run host render tests. -- Add or update tests proving overlay state does not affect final rendered output. -- Run `rg -n "debug overlay|overlay|DebugOverlay" crates specs discussion/workflow` and inspect remaining references. - -## Risks - -- Removing overlay code can accidentally remove useful telemetry; keep non-render diagnostics intact. -- The word overlay may also refer to Game HUD/primitives; distinguish those from host debug overlay. - -## Affected Artifacts - -- Host desktop modules under `crates/` -- Host/render tests diff --git a/discussion/workflow/plans/PLN-0085-pbs-stdlib-and-syscall-declarations.md b/discussion/workflow/plans/PLN-0085-pbs-stdlib-and-syscall-declarations.md deleted file mode 100644 index 032f4d90..00000000 --- a/discussion/workflow/plans/PLN-0085-pbs-stdlib-and-syscall-declarations.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -id: PLN-0085 -ticket: render-frame-packet-boundary -title: PBS Stdlib and Syscall Declarations -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Update PBS Game and Shell stdlibs so high-level APIs map to app-mode-specific ABI domains: Game uses `composer.*` and `gfx2d.*`; Shell uses `gfxui.*`. - -## Target - -PBS authors can keep ergonomic high-level APIs where appropriate, but generated or declared syscalls use the correct DEC-0030 ABI domain for the active app mode. - -## Scope - -- Update Game stdlib declarations for `composer.*` and `gfx2d.*`. -- Update Shell stdlib declarations for `gfxui.*`. -- Remove old primitive `gfx.*` ABI declarations. -- Keep author-facing wrappers clear about mode-specific behavior. -- Update examples/tests that compile PBS code. - -## Out of Scope - -- Runtime implementation of the syscall handlers. -- New visual APIs beyond the domain split. -- Backward compatibility for old primitive `gfx.*` ABI declarations. -- Widget/layout redesign. - -## Execution Sequence - -1. Inventory PBS stdlib files and syscall declaration generation. -2. Map Game scene/camera/sprites/HUD APIs to `composer.*`. -3. Map Game primitive APIs to `gfx2d.*`. -4. Map Shell primitive APIs to `gfxui.*`. -5. Remove or fail old primitive `gfx.*` ABI declarations. -6. Update PBS compile tests, examples, and syscall metadata expectations. - -## Acceptance Criteria - -- PBS Game code emits only Game-allowed render ABI domains. -- PBS Shell code emits only Shell-allowed render ABI domains. -- Old primitive `gfx.*` ABI declarations are gone or explicitly rejected. -- Existing examples are updated to the new declarations. - -## Tests / Validation - -- Run PBS stdlib and compiler tests. -- Add Game and Shell declaration tests for emitted syscall names. -- Add negative tests for wrong-domain declarations. - -## Risks - -- User-facing API names may remain similar while ABI names change; tests must assert emitted syscall domains, not just wrapper names. -- Examples can mask stale declarations if they do not compile in both Game and Shell contexts. - -## Affected Artifacts - -- PBS stdlib files -- Syscall declaration generation -- PBS tests and examples diff --git a/discussion/workflow/plans/PLN-0086-end-to-end-render-boundary-validation.md b/discussion/workflow/plans/PLN-0086-end-to-end-render-boundary-validation.md deleted file mode 100644 index acc9350b..00000000 --- a/discussion/workflow/plans/PLN-0086-end-to-end-render-boundary-validation.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -id: PLN-0086 -ticket: render-frame-packet-boundary -title: End-to-End Render Boundary Validation -status: done -created: 2026-05-25 -ref_decisions: [DEC-0030] -tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline] ---- - -## Briefing - -Source decision: `DEC-0030`. - -Add end-to-end validation proving the new boundary works across Game and Shell modes after the granular implementation plans land. - -## Target - -Integration coverage demonstrates typed submissions, app-mode capability gates, immutable closed submissions, render-surface publication, fade removal, and visible output preservation. - -## Scope - -- Add Game end-to-end tests from PBS/syscall production to `Game2DFramePacket` and final pixels. -- Add Shell end-to-end tests from UI production to `ShellUiFramePacket` and final pixels. -- Test wrong-domain capability failures. -- Test closed-submission immutability. -- Test latest-complete-submission-wins behavior. -- Test absence of fade and host debug overlay from canonical paths. - -## Out of Scope - -- Implementing missing production code. -- New rendering features. -- Performance tuning. -- Dirty regions. - -## Execution Sequence - -1. Define representative Game and Shell scenarios that cover composer, `gfx2d`, and `gfxui`. -2. Add integration harness access to inspect closed submissions where appropriate. -3. Assert Game produces `RenderSubmission::Game2D` and Shell produces `RenderSubmission::ShellUi`. -4. Assert wrong-domain syscalls fail before mutating buffers. -5. Assert closed submissions remain unchanged after producers continue mutating the next frame. -6. Assert final pixels match existing expected output for unchanged scenarios. -7. Add searches or tests proving fade and host debug overlay are absent from the active render path. - -## Acceptance Criteria - -- Game and Shell each have end-to-end typed submission tests. -- Capability gates reject `gfxui` in Game and `composer`/`gfx2d` in Shell. -- Closed submissions are immutable snapshots. -- Latest complete submission replaces older unpublished submissions without queue growth. -- Unchanged Game and Shell scenarios remain visually equivalent. - -## Tests / Validation - -- Run full affected Rust test suites. -- Run PBS compile/runtime integration tests. -- Run pixel-equivalence tests for representative Game and Shell output. -- Run `discussion validate` once execution discussion artifacts are updated. - -## Risks - -- End-to-end tests may become brittle if they assert host details; assert the logical contract and final pixels, not internal host implementation names. -- Pixel-equivalence fixtures must account for intentional removals such as host debug overlay. - -## Affected Artifacts - -- Integration tests under `crates/` -- PBS tests -- Render fixtures From 6cb0918715721e586e60b310186b9d81b75621b0 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:29:31 +0100 Subject: [PATCH 02/47] VM and Render Parallel Execution Boundary --- discussion/index.ndjson | 19 +- ...-and-render-parallel-execution-boundary.md | 729 ++++++++++++++++++ ...ground-stack-game-pause-shell-vm-backed.md | 126 +++ ...-and-render-parallel-execution-boundary.md | 245 ++++++ ...7-fix-game2d-packet-overlay-composition.md | 62 ++ ...fine-read-only-render-resource-boundary.md | 63 ++ ...89-introduce-render-handoff-abstraction.md | 62 ++ ...N-0090-add-render-frame-pacing-contract.md | 63 ++ ...-implement-appmode-render-policy-matrix.md | 63 ++ ...-add-render-epoch-ownership-transitions.md | 64 ++ ...dd-render-telemetry-counters-and-events.md | 66 ++ .../PLN-0094-specify-async-render-boundary.md | 60 ++ ...rototype-local-render-worker-capability.md | 66 ++ ...ne-render-shutdown-and-failure-handling.md | 70 ++ ...dd-async-render-integration-test-matrix.md | 61 ++ 15 files changed, 1811 insertions(+), 8 deletions(-) create mode 100644 discussion/workflow/agendas/AGD-0040-vm-and-render-parallel-execution-boundary.md create mode 100644 discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md create mode 100644 discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md create mode 100644 discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md create mode 100644 discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md create mode 100644 discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md create mode 100644 discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md create mode 100644 discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md create mode 100644 discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md create mode 100644 discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md create mode 100644 discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md create mode 100644 discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md create mode 100644 discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md create mode 100644 discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index e00a58c9..e78d68b9 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,9 +1,12 @@ -{"type":"meta","next_id":{"DSC":39,"AGD":39,"DEC":31,"PLN":87,"LSN":47,"CLSN":1}} -{"type":"discussion","id":"DSC-0038","status":"in_progress","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-05-25","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[{"id":"AGD-0038","file":"AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25"}],"decisions":[{"id":"DEC-0030","file":"DEC-0030-logical-render-pipeline-command-boundary.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25","ref_agenda":"AGD-0038"}],"plans":[{"id":"PLN-0073","file":"PLN-0073-render-contract-specs.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0074","file":"PLN-0074-hal-render-submission-types.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0075","file":"PLN-0075-rendermanager-core.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0076","file":"PLN-0076-capabilities-and-abi-domain-split.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0077","file":"PLN-0077-composer-buffer-and-game2d-packet.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0078","file":"PLN-0078-classic2d-game-renderer-consumer.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0079","file":"PLN-0079-gfx2d-primitive-domain.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0080","file":"PLN-0080-shell-ui-packet-and-gfxui-domain.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0081","file":"PLN-0081-shell-hub-migration.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0082","file":"PLN-0082-frame-publication-and-present-boundary.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0083","file":"PLN-0083-fade-removal.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0084","file":"PLN-0084-host-debug-overlay-removal.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0085","file":"PLN-0085-pbs-stdlib-and-syscall-declarations.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0086","file":"PLN-0086-end-to-end-render-boundary-validation.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]}],"lessons":[]} +{"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} +{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} -{"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":"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":"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":"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":"discussion/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":"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":"DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"discussion/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-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-06-04","tags":["perf","runtime","telemetry"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0028","file":"discussion/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-06-04","tags":["ci","jenkins","gitea"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0021","file":"discussion/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":"discussion/lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} {"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} @@ -13,7 +16,7 @@ {"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[{"id":"AGD-0007","file":"AGD-0007-perf-runtime-telemetry-hot-path.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0005","file":"DEC-0005-perf-push-based-telemetry-model.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0005","file":"PLN-0005-perf-push-based-telemetry-implementation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} +{"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-06-04","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0010","status":"done","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"discussion/lessons/DSC-0010-perf-host-desktop-frame-pacing-and-presentation/LSN-0036-frame-publication-and-host-invalidation-must-be-separate.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]} {"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} @@ -27,10 +30,10 @@ {"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":"discussion/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":"discussion/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":"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":"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":"discussion/lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} -{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} +{"type":"discussion","id":"DSC-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-06-04","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"discussion/lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} +{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-06-04","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} {"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"discussion/lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} -{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"discussion/lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} +{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-06-04","tags":["ci","jenkins"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0020","file":"discussion/lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0030","status":"done","ticket":"internal-viewport-270p","title":"Agenda - Internal Viewport 270p (480x270)","created_at":"2026-04-27","updated_at":"2026-04-28","tags":["gfx","runtime","viewport","resolution","frame-composer","host"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0030-internal-viewport-270p/LSN-0039-resolution-baseline-changes-are-runtime-wide-contracts.md","status":"done","created_at":"2026-04-28","updated_at":"2026-04-28"}]} {"type":"discussion","id":"DSC-0031","status":"done","ticket":"runtime-mode-separation-game-system","title":"Agenda - Runtime Mode Separation: Game and System","created_at":"2026-05-11","updated_at":"2026-05-14","tags":["runtime","firmware","hub","system-apps","game-mode","scheduler"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0031-runtime-mode-separation-game-system/LSN-0040-system-pipeline-separation.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]} {"type":"discussion","id":"DSC-0032","status":"done","ticket":"system-os-lifecycle-process-task-contract","title":"Agenda - SystemOS Lifecycle, Process and Task Contract","created_at":"2026-05-14","updated_at":"2026-05-15","tags":["runtime","os","lifecycle","process","task","shell","firmware"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0032-system-os-lifecycle-process-task-contract/LSN-0041-systemos-lifecycle-authority.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/agendas/AGD-0040-vm-and-render-parallel-execution-boundary.md b/discussion/workflow/agendas/AGD-0040-vm-and-render-parallel-execution-boundary.md new file mode 100644 index 00000000..bc908293 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0040-vm-and-render-parallel-execution-boundary.md @@ -0,0 +1,729 @@ +--- +id: AGD-0040 +ticket: vm-render-parallel-execution-boundary +title: VM and Render Parallel Execution Boundary +status: accepted +created: 2026-06-04 +resolved: +decision: +tags: [runtime, renderer, vm, concurrency, architecture, perf] +--- + +# Agenda - VM and Render Parallel Execution Boundary + +## Contexto + +`DSC-0038` estabeleceu `RenderSubmission` como snapshot fechado. A lesson `LSN-0047` registra explicitamente que uma submissao fechada e o primitivo util para concorrencia futura, mesmo que o v1 ainda consuma tudo de forma sincrona. + +No codigo atual, `RenderManager` guarda apenas `latest_complete_submission`, fecha frames no fluxo da VM/runtime e publica para uma `RenderSurface` via chamada direta. Isso e correto para o v1, mas ainda nao decide como mover rasterizacao/publicacao para outra thread ou core. + +A demanda aqui e separar a VM continuando em um core/thread de execucao logica enquanto o pipeline grafico consome submissions fechadas em outro worker/core, sem acesso mutavel vivo a VM, `Hardware` ou `Gfx`. + +## Problema + +Prometeu ainda nao tem contrato operacional para paralelismo VM/render. + +A fronteira de dados ja aponta a direcao, mas faltam decisoes sobre: + +- quem cria e possui a fila ou slot de submissao; +- se a politica `latest complete submission wins` basta quando ha worker real; +- como lidar com frame pacing, backpressure e queda de frames; +- quais recursos podem ser compartilhados read-only; +- como manter determinismo da VM; +- como representar isso em desktop host, handheld proprio e testes; +- se o render worker e parte do runtime, do host, da HAL ou apenas uma abstracao futura. + +## Pontos Criticos + +### 1. A VM nao pode depender do consumidor de render + +A VM deve produzir estado/comandos e avançar segundo seu proprio budget. O render worker pode atrasar, pular frames ou publicar a ultima submissao completa, mas nao deve exigir acesso mutavel ao estado vivo da VM para rasterizar. + +### 2. Snapshot fechado nao e automaticamente thread-safe + +`RenderSubmission` owned ajuda, mas qualquer referencia indireta a assets, scene cache, bancos de glyphs ou surfaces precisa ter politica clara: copia, handle estavel, Arc/read-only, snapshot de recurso, ou acesso mediado. + +### 3. `latest complete submission wins` precisa virar protocolo + +Como principio, nao queremos fila infinita. Mas uma implementacao com worker precisa decidir se usa single-slot atomico, double/triple buffering, canal bounded, sequence numbers, acknowledgements ou outro mecanismo. + +### 4. Frame pacing muda quando render sai do fluxo principal + +Hoje tick, fechamento de frame, consumo e host presentation estao acoplados. Com worker separado, a VM pode produzir mais rapido que o render ou o render pode apresentar em cadencia diferente. Isso precisa de uma regra de tempo. + +### 5. Desktop thread nao e automaticamente modelo de handheld core + +No host desktop, a implementacao natural pode ser uma thread. No hardware proprio, pode ser outro core, DMA, coprocessador, scanout ou rotina cooperativa. A decisao deve separar contrato runtime de implementacao host. + +## Opcoes + +### Opcao A - Manter consumo sincrono, apenas preservar contrato de snapshot + +**Abordagem:** + +Nao criar worker agora. Consolidar somente as invariantes: submissions owned/fechadas, sem acesso mutavel vivo durante consumo, e testes que impedem regressao para immediate rendering. + +**Pro:** + +- menor risco; +- evita concorrencia prematura; +- mantem foco em estabilizar o novo renderer; +- ainda prepara a arquitetura. + +**Con:** + +- nao reduz latencia/custo no curto prazo; +- pode adiar decisoes dificeis sobre recursos compartilhados; +- o codigo pode voltar a assumir consumo sincrono se nao houver guardrails. + +**Maintainability:** + +Alta como etapa intermediaria, mas incompleta para a demanda de core/thread separado. + +### Opcao B - Render worker com slot bounded `latest-complete` + +**Abordagem:** + +Criar um worker de render que consome um slot/canal bounded de `RenderSubmission`. O produtor substitui a submissao pendente quando uma mais nova fica pronta; o consumidor sempre pega a ultima completa disponivel. + +**Pro:** + +- implementa a politica ja aceita; +- evita fila infinita; +- desacopla VM e rasterizacao; +- aproxima desktop e hardware proprio por contrato de single/latest slot. + +**Con:** + +- exige `Send`/ownership/read-only real nos dados; +- precisa regra de shutdown, erro e sincronizacao; +- pode mascarar frames descartados sem telemetria adequada. + +**Maintainability:** + +Boa se o protocolo for pequeno e testavel. + +### Opcao C - Fila bounded com N submissions + +**Abordagem:** + +Usar canal bounded com 2 ou 3 submissions para suavizar picos e permitir que o render trabalhe atrasado. + +**Pro:** + +- simples em desktop; +- reduz bloqueios imediatos; +- permite medicao de atraso. + +**Con:** + +- contradiz parcialmente latest-wins se N crescer; +- aumenta memoria e latencia; +- pode apresentar frames velhos; +- menos adequado para handheld com memoria restrita. + +**Maintainability:** + +Media. Pode ser util como implementacao, mas precisa de regra forte para descarte. + +### Opcao D - Render thread como detalhe exclusivo do host desktop + +**Abordagem:** + +Mover rasterizacao/publicacao para thread do host, sem mudar contrato runtime/HAL. + +**Pro:** + +- melhora desktop sem mexer no console; +- menor impacto no runtime; +- facilita experimentos. + +**Con:** + +- pode criar divergencia entre host e alvo real; +- nao resolve contrato para hardware proprio; +- risco de esconder dependencia mutavel por baixo do host. + +**Maintainability:** + +Media a baixa como solucao canonica. Boa apenas para experimento ou profiling. + +## Sugestao / Recomendacao + +A recomendacao inicial e uma decisao em duas fases: + +1. Fechar agora as invariantes da Opcao A como contrato obrigatorio. +2. Planejar a Opcao B como primeiro modelo real de worker quando a base estiver pronta. + +O contrato desejado deve ser: + +```text +VM/runtime core + builds domain buffers + closes RenderSubmission snapshot + publishes latest complete submission to bounded handoff + +render worker/core + takes latest complete submission + consumes immutable packet/resource handles + rasterizes/publishes through RenderSurface + reports telemetry/backpressure +``` + +O worker nao deve receber `&mut Hardware`, `&mut Gfx`, `&mut VirtualMachineRuntime` ou ponte que permita voltar ao estado vivo da VM. + +## Perguntas Respondidas + +- [x] O handoff deve ser single-slot latest-wins, canal bounded N=2, ou outro protocolo? + - Resposta: single-slot latest-wins, sem bloquear VM/produtor. +- [x] Quais tipos dentro de `RenderSubmission` precisam ser `Send + Sync` ou owned? + - Resposta: submissions/packets cruzam a fronteira como dados owned pequenos; APIs read-only compartilhadas de bancos/cache precisam provar seguranca de compartilhamento antes do worker real. +- [x] Como assets, scene cache e glyph banks cruzam a fronteira sem copia pesada? + - Resposta: por IDs/handles estaveis e APIs read-only; bancos sao imutaveis por contrato, e o viewport cache pertence ao core logico. +- [x] Quem e dono do worker: `RenderManager`, host desktop, SystemOS service ou HAL? + - Resposta: `SystemOS`/lifecycle decide politica/AppMode; `RenderManager` coordena handoff, epoch, ownership e telemetry; host/HAL fornece capacidades e execucao concreta. +- [x] O render worker pode bloquear a VM ou apenas descartar/substituir submissions? + - Resposta: nao bloqueia a VM/produtor; descarta/substitui pending submissions conforme latest-wins. +- [x] Como medir frames produzidos, consumidos, descartados e apresentados? + - Resposta: counters e ultimos frame IDs separados para produced, replaced/dropped, consumed, presented, repeated, errors e stale epoch discards. +- [x] Como shutdown, crash, mode transition e troca de cart interagem com worker? + - Resposta: transicoes incrementam epoch/generation, invalidam pending/current obsoletos e impedem present de frames do owner anterior. +- [x] Qual parte deve ser especificada agora e qual deve esperar implementacao real? + - Resposta: especificar agora o contrato de packet fechado, handoff latest-wins, frame pacing, AppMode policy, epoch ownership e telemetry minima; implementacao concreta do worker/render backend fica para decision/plan posterior. + +## Grandes Problemas Esperados + +### Topico 1 - Game2DFramePacket como snapshot completo + +**Status:** direcao respondida; execucao pendente. + +**Pergunta de fechamento:** o consumidor de `RenderSubmissionPacket::Game2D` consegue compor o frame completo a partir do packet fechado, sem depender de caminho alternativo por estado vivo? + +Hoje `Hardware::publish_render_submission` ignora parte do `Game2DFramePacket` quando ha scene ativa, porque chama `frame_composer.render_frame(&mut self.gfx)` em vez de consumir o packet completo. Isso quebra a premissa de que o worker pode receber um snapshot fechado e rasterizar sem estado vivo. + +Antes de qualquer render worker, `Game2DFramePacket` precisa ser a fonte completa de composicao: scene/layers/sprites, `gfx2d` overlay e publicacao. + +**Direcao atual:** nao ha debate arquitetural relevante restante para este topico. `gfx2d` deve ser overlay apos a composicao Game 2D, e o consumidor de `RenderSubmissionPacket::Game2D` deve aplicar a mesma ordem em frames com scene ativa e sem scene ativa. O que falta e corrigir o caminho atual que contorna o packet quando ha scene ativa e adicionar teste de regressao cobrindo scene ativa + primitive overlay. + +### Topico 2 - Recursos pesados e fronteira read-only + +**Status:** direcao respondida. + +**Pergunta de fechamento:** como scene cache, glyph banks, memory banks, assets e recursos futuros cruzam a fronteira sem `&mut Hardware`, `&mut Gfx` ou `FrameComposer` vivo? + +`RenderSubmission` e owned, mas a composicao real ainda depende de `FrameComposer`, scene cache, resolver, glyph banks, memory banks e `Gfx`. Um worker nao pode receber `&mut Hardware` nem consultar `FrameComposer` vivo. Precisamos decidir como recursos pesados entram no snapshot: handles estaveis, `Arc` read-only, snapshots pequenos, ou acesso mediado. + +**O que falta discutir:** o ponto nao e copiar todos os recursos pesados para dentro do packet. Isso seria caro e provavelmente errado. O que falta e separar tres categorias: + +- estado de frame que deve estar no packet fechado: camera, scene binding, sprites, HUD, primitives, copy/update requests ja calculados; +- recursos residentes grandes que podem ser referenciados por handle/slot e lidos via `Arc`/read-only: glyph banks, scene banks, caches materializados; +- estado mutavel de preparacao que deve ficar no core logico antes do handoff: resolver update, cache refresh, asset installation, bank residency changes. + +**Direcao provavel:** o render worker deve receber um snapshot pequeno com comandos e handles estaveis, mais acesso read-only aos bancos/caches necessarios. A atualizacao de resolver/cache e a instalacao/troca de assets devem acontecer antes do handoff, no core logico ou em um servico proprietario, nunca durante rasterizacao no worker. + +**Perguntas respondidas:** + +- [x] O `Game2DFramePacket` deve carregar apenas `BoundScenePacket { bank_id }` ou tambem um snapshot/read handle do cache materializado? + - Resposta: IDs somente. Os bancos sao imutaveis por definicao; o packet nao deve capturar handles pesados por frame. +- [x] O cache de viewport pertence ao produtor logico, ao render worker, ou a um recurso compartilhado read-only apos refresh? + - Resposta: o viewport cache pertence ao core logico. O render worker deve acessa-lo apenas por uma API de leitura. +- [x] Uma troca de asset/bank pode invalidar recursos enquanto uma submission antiga ainda esta sendo rasterizada? + - Resposta: a submission aponta por ID para o recurso. Se o recurso for trocado, a submission ainda pode executar e o resultado pode ficar confuso/diferente. Disciplina de troca de assets e responsabilidade do dev ou framework. +- [x] O handoff deve capturar `Arc`/`Arc` por frame para garantir liveness, ou apenas IDs de slot com regra de nao substituir enquanto houver frame em voo? + - Resposta: somente IDs. Nao vamos garantir integridade do resultado por captura de recurso; disciplina de troca e responsabilidade do dev ou framework. +- [x] Quais tipos precisam provar `Send + Sync` antes de qualquer worker real? + - Esclarecimento: esta pergunta e tecnica. Em Rust, qualquer dado/API compartilhado entre threads precisa ser seguro para envio/leitura concorrente. Com a resposta "IDs somente" no packet, a exigencia sai do `Game2DFramePacket` pesado e recai principalmente sobre a API de leitura dos bancos/cache e sobre o protocolo de handoff. + - Resposta: essa prova fica como criterio tecnico de implementacao do worker real; a decisao arquitetural e que a fronteira compartilhada deve ser owned ou read-only, sem `&mut Hardware`, `&mut Gfx`, `FrameComposer` vivo ou acesso mutavel a VM. + +**Direcao respondida:** packet pequeno com IDs, bancos imutaveis por contrato, viewport cache pertencente ao core logico e acesso do worker por API read-only. O sistema nao deve tentar garantir integridade visual de submissions antigas contra troca disciplinarmente incorreta de assets/banks; essa disciplina pertence ao dev/framework. + +### Topico 3 - Protocolo de handoff latest-wins + +**Status:** direcao respondida. + +**Pergunta de fechamento:** qual protocolo substitui o `Option` local quando houver outro core/thread? + +O `latest_complete_submission` atual e apenas um `Option` dentro do runtime. Para outro core/thread, isso precisa virar protocolo: single-slot latest-wins, canal bounded, sequence numbers, descarte, acknowledgement e telemetria. + +**O que falta discutir:** este topico define o contrato de handoff, nao a politica de frame pacing completa. Precisamos escolher como uma submission sai do core logico e chega ao render worker: + +- single-slot latest-wins: produtor substitui a submission pendente por uma mais nova, e o worker consome a ultima disponivel; +- canal bounded N=1/2: produtor tenta enviar, e o protocolo decide se descarta antigo, descarta novo ou bloqueia; +- acknowledgement: worker informa qual `frame_id` consumiu/apresentou, ou o produtor apenas publica e esquece; +- ownership: a submission e movida para o handoff ou clonada do `RenderManager`; +- sequence numbers: `frame_id` basta ou precisamos de counters separados para produzido/consumido/apresentado; +- escopo por pipeline: Game pode usar handoff; Shell pode publicar sincrono. + +**Direcao provavel:** para o primeiro modelo real, preferir single-slot latest-wins, sem fila crescente e sem bloquear a VM. `RenderManager` deixa de ser apenas dono de `Option` e passa a publicar uma submission fechada para um handoff pequeno. O worker pega a ultima submission disponivel; se uma submission antiga for substituida antes do consumo, isso conta como drop/substituicao mensuravel. + +**Direcao aceita:** single-slot latest-wins, sem bloquear a VM/produtor. + +**Perguntas respondidas:** + +- [x] O slot guarda exatamente uma pending submission alem da currently rendering, ou apenas a ultima submission atomica disponivel? + - Resposta: o handoff guarda uma pending submission owned. Se o worker ja pegou uma submission, ela esta fora do slot e em consumo. O slot representa apenas a proxima/latest pending. +- [x] Quando o worker esta renderizando frame N e o core produz N+1 e N+2, N+1 deve ser descartado? + - Resposta: sim. `N+2` substitui `N+1` se `N+1` ainda nao foi retirado pelo worker. Isso e counted como replaced/dropped-before-consume. +- [x] O produtor precisa saber que uma submission foi consumida, ou so que foi substituida/publicada? + - Resposta: o produtor nao espera acknowledgement para continuar. O worker deve reportar consumed/presented como telemetria assíncrona, mas isso nao bloqueia nem participa da progressao da VM. + - Observacao: a preocupacao restante e a semantica do `tick`. Hoje VM e render estao dentro do mesmo tick sincrono. Com worker, o tick deve fechar/publicar a submission, mas nao esperar ACK de render. A relacao entre tick, present efetivo e telemetria fica para o Topico 4. +- [x] `frame_id` e suficiente para telemetria, ou precisamos de contadores separados de produced/dropped/consumed/presented? + - Resposta: `frame_id` e suficiente para ordenacao, mas nao para observabilidade. Precisamos de contadores separados: produced, replaced/dropped-before-consume, consumed, presented, render_errors/present_errors, e ultimos frame ids por etapa. +- [x] Shell UI fica fora do protocolo worker por default, ou usa o mesmo handoff quando configurado? + - Resposta: Shell UI fica fora por default e pode continuar sincrono/local. O mesmo protocolo pode ser reutilizado se uma configuracao futura quiser worker para Shell, mas a politica inicial e por pipeline/AppMode. + +**Direcao respondida:** o protocolo base e single-slot latest-wins com ownership moved para o handoff, pending slot substituivel, worker consumindo owned submission, VM/produtor nunca bloqueado, ack apenas para telemetria e politica por pipeline. + +Confirmacoes de 2026-06-04: + +- ownership moved para o handoff: aceito; +- produtor sem ACK bloqueante: aceito, com a preocupacao de definir a nova semantica de tick; +- contadores separados: seguir com essa direcao; +- Shell UI fora do worker por default: aceito. + +### Topico 4 - Frame pacing e backpressure + +**Status:** direcao respondida. + +**Pergunta de fechamento:** a VM bloqueia, descarta, substitui ou apenas mede atraso quando o render worker esta atrasado? + +Hoje tick, fechamento, raster e present acontecem no mesmo fluxo. Com worker separado, a VM pode produzir frames mais rapido que o render consome, ou o render pode apresentar em outra cadencia. Precisamos decidir se a VM bloqueia, descarta, substitui, ou apenas mede atraso. + +**Direcao em discussao:** se o render real for para worker/core, o `present` passa a ser responsabilidade do worker/render surface e deve mirar a cadencia de display, inicialmente 60Hz. O worker nao consome uma fila crescente; ele consome a latest submission disponivel pelo protocolo single-slot e apresenta em sua cadencia. + +O core logico/VM deixa de precisar reservar tempo de render dentro do mesmo tick sincrono, mas nao deve rodar livremente. O frame logico da VM e parte do modelo de tempo do jogo: animacoes, input, scripts e simulacao dependem dele. Portanto, a VM deve continuar paced por frame logico, mirando 1:1 com a cadencia do worker/display sempre que possivel. + +Idealmente, ha relacao 1:1: + +```text +1 logical VM frame +-> 1 RenderSubmission +-> 1 presented worker frame +``` + +Mas o contrato nao deve depender de sempre conseguir isso. A direcao revisada e: + +- o worker/render surface e dono do `present` em 60Hz; +- o worker apresenta o ultimo frame processado/disponivel; +- quando o worker aceita/pega uma nova submission para apresentacao, isso avanca o frame counter visivel; +- se a VM ainda nao disponibilizou uma nova submission, o worker mantem a ultima renderizacao e registra skip/no-new-frame; +- a VM nao roda frames logicos ilimitados a frente do display; ela so deve iniciar novo frame logico quando a cadencia permitir produzir o proximo frame esperado; +- se o worker ainda esta processando um frame anterior, a VM pode preparar/publicar o proximo frame logico dentro do limite 1:1, mas nao deve acumular multiplos frames logicos pendentes. + +Isso muda a leitura do latest-wins: ele continua util para substituir uma pending submission que ainda nao foi pega pelo worker, mas nao deve virar autorizacao para a VM rodar muito a frente do display. O objetivo e desacoplar custo de raster/present, nao desacoplar o tempo logico do jogo da cadencia de frame. + +Se o worker nao rasterizar dentro do budget de 60Hz, o sistema deve contar atraso/drop/skip em telemetria. A VM nao deve bloquear esperando ACK de render, mas deve respeitar o pace de frame logico definido pelo display/worker. + +Refinamento importante: + +A VM nao espera ACK de render/present de uma submission especifica, mas deve esperar a autorizacao de pace derivada do frame counter/display. Ou seja, o render worker nao confirma "terminei seu frame N, pode seguir"; em vez disso, o sistema avanca uma cadencia de frame e a VM so inicia o proximo logical frame quando essa cadencia permite. Isso evita que a VM rode livremente sem transformar present em bloqueio sincrono. + +### Corner cases de frame pacing + +1. Worker em 60Hz, VM pronta antes do proximo refresh. + - A VM deve ficar aguardando o proximo frame pace para iniciar novo logical frame. + - Nao deve produzir multiplas submissions futuras. + +2. Worker apresenta refresh sem nova submission. + - O worker repete/mantem a ultima renderizacao. + - O sistema registra skip/no-new-frame. + - O frame counter visivel pode avancar, mas a VM nao deve simular um novo logical frame retroativamente. + +3. VM produz submission, worker ainda esta rasterizando frame anterior. + - A submission nova fica no single-slot pending. + - Se outra submission fosse produzida antes do worker pegar a pending, ela substituiria a pending; porem o pace da VM deve impedir esse acúmulo na pratica. + +4. Worker pega pending submission para rasterizar. + - A pending sai do slot e passa a pertencer ao worker. + - A VM nao espera ACK dessa submission, mas so inicia novo logical frame quando o pace permitir. + +5. Worker perde prazo de 16.67ms. + - O sistema registra atraso/render-overrun. + - A VM nao deve tentar compensar produzindo frames extras. + - A proxima permissao de logical frame deve seguir a politica de pace, nao backlog. + +6. VM demora mais que a janela esperada de logical frame. + - O runtime nao deve encerrar o frame por budget como mecanismo normal. + - `FRAME_SYNC` continua sendo o fim canonico do logical frame. + - O sistema registra logical-frame-overrun para certificacao/watchdog/diagnostico. + - O worker continua apresentando a ultima renderizacao. + - Quando a VM finalmente publica a submission, ela representa o proximo logical frame sequencial, nao um frame pulado. + +7. Mode transition Game/Shell enquanto ha pending ou currently-rendering submission. + - A submission antiga nao deve ser apresentada depois que o modo mudou, a menos que a politica de transition permita explicitamente. + - O handoff precisa carregar generation/mode epoch ou equivalente para invalidar frames obsoletos. + +8. Cartridge/app troca enquanto worker tem frame antigo. + - Submissions de app/cart anterior devem ser descartadas por generation/epoch. + - O worker deve limpar ou substituir a surface conforme politica de lifecycle. + +9. Crash/panic durante logical frame antes de publicar submission. + - O worker segue apresentando ultimo frame valido ate receber crash/shell submission. + - O crash path deve poder invalidar pending game frame. + +10. Crash/panic no render worker. + - Nao pode corromper determinismo da VM. + - Deve virar telemetria/erro de render e possivelmente fallback para sync/local render ou crash surface. + +11. Input sampling. + - Input deve ser amostrado no inicio do logical frame autorizado pelo pace. + - Se o worker repetir frame por falta de submission nova, nao deve haver novo input sampling/logical update correspondente. + +12. Frame counter semantics. + - Precisamos separar pelo menos logical_frame_id, produced_submission_frame_id e presented_frame_id. + - O frame counter que autoriza a VM deve representar cadencia logica/display, nao ACK de uma submission especifica. + +### Avaliacao de desenho + +O desenho fica mais complexo se o render worker tambem virar a autoridade do frame counter que libera a VM. Isso mistura duas responsabilidades: + +- cadencia logica do sistema; +- consumo/rasterizacao/publicacao de submissions. + +Uma alternativa mais limpa e introduzir uma autoridade de pace separada, por exemplo `FrameClock` ou scheduler de frame, que emite ticks de 60Hz. A VM e o render worker observam essa cadencia, mas nenhum dos dois e dono exclusivo dela. + +Modelo preferido para reduzir complexidade: + +```text +FrameClock / frame scheduler + emits 60Hz frame ticks + +VM/core logico + runs at most one logical frame per frame tick + samples input on authorized logical frame + publishes at most one latest submission + +render worker/surface + presents on display cadence + consumes latest available submission + repeats last rendered frame if no new submission exists +``` + +Assim, a VM nao espera ACK do worker e tambem nao roda livremente. Ela espera o frame clock. O worker nao precisa "desbloquear" a VM; ele apenas consome e apresenta. Isso preserva 1:1 como alvo sem acoplar progresso logico a sucesso/falha de rasterizacao. + +Conclusao provisoria: preferir `FrameClock`/scheduler como autoridade de cadencia, `RenderManager` como produtor de submissions, e render worker como consumidor/presenter. Evitar que o worker seja dono do frame counter canonico. + +Refinamento sobre Shell/App lifecycle: + +O `FrameClock`/scheduler nao deve impor um game loop ao Shell. O loop de um game e diferente do lifecycle de um app UI, e em Prometeu o game loop e entregue no hardware/runtime como parte do modelo de console. Shell/SystemOS deve ser tratado como lifecycle/event-driven: input, janelas, foco, app lifecycle e invalidacao visual determinam quando ha trabalho logico ou render novo. + +Portanto, `FrameClock` deve autorizar cadencia de logical frame para pipelines de game, mas Shell UI nao deve necessariamente consumir um frame logico de VM a cada 60Hz. Para Shell: + +```text +SystemOS/Shell lifecycle events +-> input/window/app updates when needed +-> build ShellUiFramePacket on invalidation/event +-> present/repeat last surface as needed +``` + +Isso reforca que `FrameScheduler` nao e "scheduler da VM". Ele e uma fonte de cadencia para workloads que precisam de frame pacing, especialmente Game2D/Game3D. Shell/App lifecycle pode usar outro modelo, com render por invalidacao ou por eventos, ainda que o host/display continue apresentando em 60Hz. + +Resposta sobre budget e frames lentos: + +`FRAME_SYNC` e o fim canonico do logical frame. O budget antigo era um mecanismo grosseiro para pausar a VM e permitir que o tick continuasse; nesse desenho ele nao deve ser usado como forma normal de encerrar ou cortar um frame logico. + +Budget passa a existir para certificacao, watchdog, diagnostico e deteccao de overrun. Se um logical frame demora mais que aproximadamente 16.67ms, nao pulamos logical frames da VM para alcançar o display. Frames logicos devem ser sequenciais. O resultado visivel e stutter/repeticao do ultimo frame pelo worker, nao time-skip semantico. + +```text +VM logical frame N demora demais +-> worker repete ultimo frame disponivel +-> runtime registra overrun +-> VM continua ate FRAME_SYNC/trap/halt/watchdog fatal +-> submission publicada e N, nao N+k +``` + +**Perguntas respondidas:** + +- [x] O worker deve repetir o ultimo frame quando nao houver submission nova no refresh de 60Hz? + - Resposta: sim. O worker mantem a ultima renderizacao e registra skip/no-new-frame. +- [x] A VM deve continuar limitada a um logical frame por tick de sistema, ou pode produzir multiplas submissions entre presents? + - Resposta: a VM nao deve produzir multiplos frames logicos a frente do display. O alvo e 1 logical frame por frame worker/display, com no maximo uma pending submission pelo protocolo latest-wins. +- [x] `FRAME_SYNC` continua sendo o limite canonico de logical frame mesmo sem present sincrono? + - Resposta: sim. `FRAME_SYNC` continua delimitando logical frame da VM. O present deixa de ser sincrono, mas o frame logico continua paced. +- [x] Qual budget permanece na VM: ciclos/instrucoes por logical frame, tempo real, ou ambos? + - Resposta: `FRAME_SYNC` encerra o logical frame. Budget nao e mecanismo normal de tick/frame advance. Budget/ciclos/tempo real ficam para certificacao, watchdog, diagnostico e overrun policy. +- [x] Como detectar game rodando rapido demais se o render worker esta desacoplado? + - Resposta: o runtime nao permite que a VM rode livremente a frente do frame counter/worker. A telemetria deve contar produced, accepted/presented e skips para detectar desalinhamento. + +### Topico 5 - Politica por pipeline/AppMode + +**Status:** direcao respondida; detalhamento de lifecycle movido para `DSC-0041`. + +**Pergunta de fechamento:** quais pipelines devem poder usar worker e quais podem continuar sincronos/localmente? + +Shell UI pode continuar sincrona/local. O contrato comum deve ser packet fechado, mas a politica de execucao deve variar por pipeline. Forcar Shell UI para render worker pode aumentar complexidade sem ganho. + +**O que precisamos acertar:** este topico define a matriz de politica por `AppMode`/pipeline. O sistema hoje tem `AppMode::Game` e `AppMode::Shell`, mas esses modos nao devem receber a mesma semantica de loop/render. + +Direcao ja encaminhada: + +- `Game2D` e futuro `Game3D` sao workloads frame-paced. Devem poder usar `FrameScheduler` e render worker. +- `ShellUi` e Shell/SystemOS sao lifecycle/event-driven. Devem poder continuar sincronos/localmente e renderizar por invalidacao/evento. +- O contrato comum continua sendo `RenderSubmission` fechado. +- A politica de execucao do consumidor pode variar por pipeline/AppMode. + +**Perguntas respondidas:** + +- [x] `AppMode::Game` sempre usa `FrameScheduler`, mesmo se o game nao tiver render pesado? + - Resposta: sim. Game e frame-paced por definicao. Futuramente `AppMode::Game` pode virar algo mais explicito como `AppMode::Game2D`, mas cada AppMode deve ter uma politica de render explicita. +- [x] `AppMode::Game` deve usar render worker por default, ou isso deve ser capability/configuracao de host? + - Resposta: Game usa render worker por default quando o host/runtime suporta, com fallback sincrono/local. +- [x] `AppMode::Shell` deve ficar explicitamente fora do `FrameScheduler` logico de game? + - Resposta: sim. Shell nao herda game loop. +- [x] Shell UI deve publicar sincrono/local por default, com worker apenas opt-in? + - Resposta: sim. Shell UI e sincrono/local por default; worker so como opt-in futuro. +- [x] Apps VM-backed dentro do Shell seguem lifecycle de Shell ou podem declarar workload frame-paced proprio? + - Resposta: seguem lifecycle de Shell, sem direito a declarar workload frame-paced proprio. +- [x] Firmware screens como splash/crash seguem politica Shell/local ou politica propria? + - Resposta: seguem politica Shell/local. +- [x] A escolha de politica fica no `RenderManager`, no SystemOS/lifecycle, ou em uma camada de host/runtime configuration? + - Resposta: SystemOS/lifecycle decide o perfil/AppMode; `RenderManager` executa a politica; host/runtime fornece capacidades e fallback. + +**Direcao respondida:** cada AppMode deve ter politica explicita de render. Game atual e frame-paced e pode usar worker por default quando disponivel. Shell e lifecycle/event-driven e sincrono/local por default. Apps VM-backed dentro do Shell seguem lifecycle de Shell, sem opt-in para game pacing proprio. + +### Topico 6 - Lifecycle, transitions e ownership da surface + +**Status:** direcao respondida; detalhamento de lifecycle movido para `DSC-0041`. + +**Pergunta de fechamento:** quem possui a surface/submission durante troca Game/Shell, crash, splash, troca de cart e shutdown? + +Troca Game/Shell, crash screen, splash, troca de cart e shutdown precisam saber quem possui a surface e qual submission ainda pode ser publicada. O worker nao pode ficar publicando frame velho depois que o modo ou cartridge mudou. + +**O que precisamos acertar:** quando existe worker/render assíncrono, podem existir submissions em tres estados: + +- pending no single-slot; +- currently-rendering no worker; +- ultimo frame apresentado/mantido na surface. + +Lifecycle precisa dizer o que acontece com cada um quando o sistema muda de dono visual. + +**Direcao provavel:** toda submission deve carregar um contexto de ownership alem de `frame_id` e `app_mode`, por exemplo `render_epoch`/`generation`, `app_id` ou `surface_owner`. O `RenderManager`/SystemOS incrementa essa generation quando troca app, cart, modo ou crash state. O worker deve descartar pending/current frames cujo ownership nao combine com o epoch ativo antes de apresentar. + +**Perguntas respondidas para o escopo desta agenda:** + +- [x] Troca Game -> Shell invalida imediatamente pending/current Game submissions? + - Resposta: sim. A troca de foreground visual owner incrementa epoch/generation e torna obsoletas as submissions do owner anterior. +- [x] Troca Shell -> Game deve limpar surface ou pode manter ultimo Shell frame ate primeiro Game frame? + - Resposta: pode manter a ultima surface apresentada ate a primeira submission valida do novo owner, desde que nenhum frame antigo seja apresentado novamente como nova submission. Limpar e uma politica visual opcional. +- [x] Crash screen invalida pending/current Game frame antes de publicar crash Shell/local frame? + - Resposta: sim. Crash troca o owner visual para a tela de erro/sistema e deve invalidar frames pendentes/correntes do Game. +- [x] Splash/crash/hub usam uma surface compartilhada ou ownership proprio? + - Resposta: podem usar a mesma surface fisica, mas precisam de ownership logico proprio via epoch/generation/surface owner. +- [x] Troca de cartridge/app incrementa `render_epoch` mesmo se `AppMode` continuar igual? + - Resposta: sim. `AppMode` nao e identidade suficiente; troca de app/cart precisa invalidar submissions antigas. +- [x] Worker pode terminar raster de um frame obsoleto, mas deve checar epoch antes de present? + - Resposta: sim. O worker pode desperdiçar trabalho ja iniciado, mas nao pode apresentar se o epoch/owner nao for mais atual. +- [x] Quem incrementa epoch: SystemOS/lifecycle, `RenderManager`, host, ou todos via API central? + - Resposta: `SystemOS`/lifecycle decide a transicao semantica; `RenderManager` deve oferecer a API central que incrementa/aplica epoch. Host nao decide ownership. +- [x] Shutdown deve drenar worker, cancelar pending/current, ou apenas parar present? + - Resposta: shutdown deve parar present, descartar pending, invalidar current por epoch/stop token e encerrar/joinar o worker de forma bounded. Nao deve drenar frames obsoletos para a tela. + +**Levantamento de material existente:** + +- `DSC-0031` / `LSN-0040` ja estabeleceu `AppMode` como discriminador de perfil. Game e Shell/System usam a mesma VM/transporte internamente, mas nao compartilham o mesmo perfil autoral. Game segue pipeline de jogo; Shell/System segue pipeline Runtime/Hub. +- `DSC-0032` / `LSN-0041` ja estabeleceu `SystemOS` como autoridade semantica de lifecycle para task/process. Firmware nao deve coordenar manualmente task/process fora dessa fronteira. +- `DSC-0035` / `LSN-0044` ja estabeleceu que Shell liveness depende da janela focada pertencer ao proprio task; perder a janela elegivel fecha o Shell task via lifecycle e volta ao Hub. +- `DSC-0036` / `LSN-0045` ja estabeleceu o fluxo visual/lifecycle `Hub/Home -> Shell app -> task-owned window -> close -> lifecycle close -> Hub/Home`. +- `DSC-0038` / `LSN-0047` ja estabeleceu que `RenderManager` coordena active app mode, submission closure, transitions, capabilities e publication flow, mas sem virar renderer. + +**Estado atual de implementacao:** + +- Firmware possui estados explicitos (`GameRunning`, `ShellRunning`, `AppCrashes`, splash/hub/load) e `change_state` executa `on_exit`/`on_enter` imediatamente para evitar frames vazios. +- `LoadCartridgeStep` roteia `AppMode::Shell` para task/window Shell e `ShellRunning`; caso contrario cria task de game e entra em `GameRunning`. +- `GameRunningStep` exige task foreground e executa `ctx.os.vm().tick(...)`. +- `ShellRunningStep` exige task foreground e janela focada pertencente ao task; caso contrario fecha pelo lifecycle e volta ao Hub. +- `RenderManager` atual tem `RenderTransitionState::Pending { from, to }`, mas isso e placeholder/no-op: nao ha epoch/generation, invalidacao de pending/current frames, nem ownership de surface. + +**Gap para este topico:** as politicas de lifecycle Game/Shell existem, mas ainda nao foram propagadas para ownership visual assíncrono. Para `DSC-0040`, a direcao e suficiente: toda transicao de foreground visual owner passa por uma API central do `RenderManager`, incrementa epoch/generation, invalida pending/current do owner anterior e impede present de frames obsoletos. + +### Caso principal - Home durante Game e retorno ao Game + +Cenario esperado: + +```text +GameRunning +-> usuario aperta Home +-> Game e suspenso/pausado como task foreground +-> Hub/Home assume Shell UI +-> usuario navega/checa algo no Shell +-> usuario retorna ao Game +-> Game volta como foreground +``` + +Mesmo sem transicoes suaves, esse ciclo precisa funcionar. O estado atual cobre partes do modelo, mas nao o ciclo completo: + +- ja existe `GameRunningStep` para game fullscreen foreground; +- ja existe `HubHomeStep` e `ShellRunningStep` para Hub/Shell lifecycle; +- ja existe lifecycle authority em `SystemOS`; +- ja existe Shell task/window liveness; +- ainda nao ha evidencia de um fluxo Home durante `GameRunning` que suspenda o game, entre no Hub, e depois retorne ao mesmo game; +- o `RenderManager` so possui `RenderTransitionState::Pending { from, to }` como placeholder; nao ha ownership/epoch para invalidar frames de Game enquanto o Hub assume a surface. + +Politica provavel para esse caso: + +- Home em Game nao fecha o game; suspende/pausa o game task/process. +- Ao entrar no Hub, qualquer pending/current Game submission deve ser invalidada por epoch/generation antes de Shell/Hub assumir a surface. +- Hub/Shell renderiza local/sincrono por politica Shell. +- Retornar ao Game incrementa epoch novamente e retoma o game task/process. +- O primeiro frame de retorno ao Game deve vir de uma nova Game submission; ate la a surface pode manter ultimo Hub frame ou limpar conforme politica visual. +- Transicoes suaves ficam fora do escopo; o requisito minimo e ownership correto e ausencia de frame obsoleto apresentado apos troca de dono. + +Questao de escopo levantada: + +No ciclo `Game -> Shell -> Game`, pode existir um game pausado e um app Shell VM-backed aberto ao mesmo tempo. Isso cruza render ownership com lifecycle/process policy. + +Direcao provisoria para esta agenda: + +- somente um Game foreground/pausado por vez; +- somente um Shell foreground aberto por vez no v1; +- background execution fica fora deste contrato; +- Shell VM-backed durante Game pausado deve seguir lifecycle de Shell e nao ganha politica frame-paced propria; +- a VM/game pausado nao deve continuar executando frames logicos enquanto o Shell VM-backed esta foreground; +- a existencia simultanea de um Game pausado e um Shell VM-backed foreground precisa ser tratada como politica de SystemOS/lifecycle, nao como detalhe do render worker. + +Essa pergunta foi separada na `DSC-0041`. Para `DSC-0040`, o ponto minimo esta respondido: render ownership deve acompanhar o foreground visual owner e invalidar submissions do owner anterior por epoch/generation. + +### Topico 7 - Erros, drops e telemetria de render + +**Status:** direcao respondida. + +**Pergunta de fechamento:** quais eventos minimos o sistema registra para render produzido, consumido, descartado, atrasado, apresentado e com erro? + +Quando rasterizacao sai do fluxo da VM, panic/erro de render, frame drop, atraso, consumo e present precisam ser reportados sem quebrar determinismo da VM. Sem telemetria minima, latest-wins pode esconder perda de frames. + +**Direcao aceita:** render assíncrono e best-effort observavel, nao handshake semantico com a VM. A VM produz frames sequenciais no ritmo do `FrameScheduler`; o worker consome/apresenta o mais recente possivel; drops, repeats, erros e descartes por epoch/stale ownership aparecem em telemetry, mas nao alteram diretamente a semantica do programa dentro da VM. + +**Contrato minimo:** + +- a VM nao observa erro/drop de render como retorno sincrono nem como semantica de programa; +- render telemetry e preocupacao de host/runtime/system, debugger, profiler e certificacao; +- erro de raster descarta a submission afetada, registra telemetry e preserva o ultimo frame valido/surface atual quando possivel; +- erro de present registra telemetry e pode escalar para politica de host/system fault se for recorrente; +- panic no worker deve virar falha controlada de worker/backend, nao panic propagado para a VM; +- o programa dentro da VM nao deve depender de confirmacao de present para avancar logica. + +**Counters minimos:** + +- `produced_submissions`; +- `replaced_before_consume`; +- `consumed_submissions`; +- `presented_frames`; +- `repeated_presents`; +- `render_errors`; +- `present_errors`; +- `stale_epoch_discards`; +- `shutdown_discards`. + +**IDs/estado minimo:** + +- `last_produced_frame_id`; +- `last_consumed_frame_id`; +- `last_presented_frame_id`; +- `last_dropped_frame_id`; +- `last_error_frame_id`; +- `active_render_epoch`. + +**Eventos minimos:** + +- submission substituida no single-slot antes de consumo; +- worker consumiu uma submission; +- worker descartou submission por epoch/owner obsoleto; +- worker apresentou frame; +- worker repetiu ultimo frame por ausencia de submission nova; +- erro de raster; +- erro de present; +- shutdown/cancel descartou pending/current. + +## Fechamento dos Topicos + +Esta agenda pode ser considerada madura para decisao quando os sete topicos acima tiverem uma resposta de direcao. As respostas nao precisam implementar render worker agora, mas precisam ser concretas o suficiente para produzir uma decision sem reabrir a arquitetura base durante o plan. + +## Criterio para Encerrar + +Esta agenda pode virar decisao quando houver consenso sobre: + +- protocolo de handoff VM/render; +- ownership de submissions e recursos compartilhados; +- politica de backpressure; +- fronteira entre runtime contract e implementacao host/hardware; +- telemetria minima; +- o que fica fora de escopo antes do primeiro worker real. + +## Discussion + +Aberta em 2026-06-04 apos verificar que `DSC-0038` preparou snapshots fechados e latest-wins, mas nao decidiu thread/core separado. + +Atualizacao de 2026-06-04: + +Antes de discutir worker/thread de render, precisamos fechar uma pre-condicao de composicao do pacote atual. A pergunta operacional foi: comandos de primitivas em `gfx2d` sao desenhados apos qualquer blit/raster dos layers? + +O estado atual e misto: + +- No caminho direto `Gfx::render_game2d_frame_packet`, `render_no_scene_frame()` roda antes e depois `packet.gfx2d` e aplicado. Nesse caso, primitivas ficam depois da composicao base. +- No caminho real de `Hardware::publish_render_submission`, quando `frame_composer.active_scene_id().is_some()`, o codigo ignora o `Game2DFramePacket` recebido e chama `frame_composer.render_frame(&mut self.gfx)`. +- `FrameComposer::render_frame` atualiza cache/resolver e chama `gfx.render_scene_from_cache(...)`, que faz o blit/raster dos layers, mas nao aplica `packet.gfx2d`. + +Conclusao provisoria: para frames com scene ativa, ainda nao ha garantia de que `gfx2d` seja desenhado apos os layer blits; na pratica, esses comandos podem ser descartados nesse branch. Isso deve ser resolvido antes de transformar `RenderSubmission` em fronteira de handoff para outro worker/core, porque o worker precisa consumir o pacote fechado completo, nao reconstruir comportamento a partir de estado vivo do `FrameComposer`. + +Resposta de direcao em 2026-06-04: + +As primitivas `gfx2d` devem funcionar como overlay do frame Game 2D. Elas precisam ser desenhadas apos a composicao canonical de scene/layers/sprites tanto em frames sem scene ativa quanto em frames com scene ativa. O comportamento esperado e: + +```text +Game2D composer/layers/sprites +-> gfx2d primitives overlay +-> present/publication +``` + +Isso implica que o branch com scene ativa em `Hardware::publish_render_submission` nao deve contornar o `Game2DFramePacket` nem perder `packet.gfx2d`. A solucao futura deve fazer o consumidor de `RenderSubmissionPacket::Game2D` aplicar a mesma ordem de composicao nos dois caminhos. + +Essa regra tambem reforca a fronteira necessaria para render worker/thread: o pacote fechado deve conter tudo que o consumidor precisa para compor o frame visivel. O worker nao deve depender de chamar um caminho alternativo baseado em estado vivo do `FrameComposer` que ignora parte do packet. + +Atualizacao sobre Shell UI em 2026-06-04: + +`ShellUi` e `Game2D` sao separados no contrato e no roteamento, mas compartilham os mesmos helpers finais de desenho no `Gfx`. `ShellUiFramePacket` contem comandos `gfxui`, enquanto `Game2DFramePacket` contem `composer` + `gfx2d`. No consumidor local, `apply_gfxui_command` e `apply_gfx2d_command` chamam os mesmos primitives finais (`clear`, `fill_rect`, `draw_line`, `draw_text`, etc.). + +Mover o render real para outro core/thread nao deve atrapalhar Shell UI se a fronteira for respeitada: Shell update/layout/input continuam no core logico, produzem um `ShellUiFramePacket` fechado, e o render worker consome apenas esse snapshot imutavel. + +O risco especifico do Shell UI nao e rasterizar em outro core. O risco e deixar o worker depender de estado vivo de Shell/SystemOS, janelas, ponteiros ou input enquanto desenha. Hoje o Hub monta o packet lendo estado de OS/janelas/input e publica imediatamente. Num modelo paralelo, essa montagem deve continuar antes do handoff; o worker deve receber so comandos fechados e recursos estaveis. + +Portanto, para Shell UI, a regra de handoff deve ser: + +```text +Shell/SystemOS update + input sampling +-> build ShellUiFramePacket from current UI state +-> hand off immutable ShellUi submission +-> render worker consumes commands only +``` + +Isso preserva a UI sobre primitivas sem exigir que o render worker entenda window manager, foco, input ou lifecycle. + +Direcao refinada em 2026-06-04: + +Render paralelo faz mais sentido como capacidade para workloads de game 2D/3D do que como obrigacao universal do runtime. Um app UI/Shell pode nao se beneficiar de separar rasterizacao em outro core, porque seu custo principal tende a estar em estado de UI, layout, input, foco, janelas e lifecycle, nao em um pipeline grafico pesado. + +Portanto, a decisao futura nao deve assumir que todo `RenderSubmission` passa obrigatoriamente por um worker dedicado. A politica deve poder variar por `AppMode`/pipeline: + +- `Game2D`/futuro `Game3D` podem usar handoff para render worker/core quando houver ganho real de frame time ou isolamento. +- `ShellUi` pode continuar em consumo sincrono/local se isso for mais simples, deterministico e barato. +- O contrato comum deve continuar sendo o packet fechado; a politica de execucao do consumidor pode ser diferente por pipeline. + +Essa distincao evita otimizar a Shell UI como se fosse um renderer de jogo. Para UI, o valor principal de `ShellUiFramePacket` e manter a fronteira limpa e testavel; paralelismo deve ser opcional. + +## Resolution + +Direcao da agenda respondida. Os sete topicos tem recomendacao suficiente para virar decision: + +- `Game2DFramePacket` deve ser snapshot completo, com `gfx2d` como overlay apos composicao de scene/layers/sprites; +- recursos pesados cruzam a fronteira por IDs/handles estaveis e APIs read-only, nao por handles mutaveis vivos; +- handoff base e single-slot latest-wins, sem bloquear a VM/produtor; +- frame pacing fica sob `FrameClock`/`FrameScheduler`, nao sob ACK do render worker; +- politica de render varia por `AppMode`/pipeline: Game frame-paced; Shell lifecycle/event-driven e local/sincrono por default; +- transicoes de foreground visual owner usam epoch/generation para invalidar submissions obsoletas; +- telemetria minima torna drops, repeats, erros e stale discards observaveis sem virar semantica da VM. + +Detalhes de foreground stack, Game pausado e coexistencia com Shell VM-backed foram separados na `DSC-0041`. + +## Next Step + +Transformar esta agenda em decision antes de qualquer spec ou codigo de render worker. diff --git a/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md b/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md new file mode 100644 index 00000000..973f74e2 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md @@ -0,0 +1,126 @@ +--- +id: AGD-0041 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Foreground Stack, Game Pause, and VM-Backed Shell Coexistence +status: open +created: 2026-06-05 +resolved: +decision: +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Contexto + +Durante a discussao da `DSC-0040` sobre separar VM e render worker, surgiu um caso que nao pertence somente ao render: `Game -> Shell -> Game`. + +O caso minimo e: + +- um Game esta rodando; +- o usuario aperta Home; +- o Game deve sair do foreground sem ser encerrado; +- o Hub/Shell assume a tela; +- o usuario pode abrir um app Shell, inclusive VM-backed; +- depois o usuario retorna ao mesmo Game. + +O render worker precisa saber qual visual owner pode apresentar frames, mas a decisao principal e de lifecycle: quais VMs existem, quais podem executar, o que significa pausar um Game, e como Shell apps coexistem com um Game pausado. + +Material relacionado: + +- `DSC-0031`: separacao entre perfis Game e System/Shell. +- `DSC-0032`: SystemOS como autoridade de lifecycle/process/task. +- `DSC-0035`: janelas Shell pertencem a tasks e definem liveness do app. +- `DSC-0036`: Hub/Shell prova fatias de lifecycle. +- `DSC-0040`: render ownership/epoch durante transicoes e paralelismo VM/render. + +## Problema + +Precisamos definir o contrato de foreground para impedir ambiguidades como: + +- Game pausado continuando a executar frames logicos enquanto Shell esta no foreground; +- Shell VM-backed competindo com Game por pacing, input, superficie ou render ownership; +- multiplos Shells foreground ou multiplos Games ativos sem uma politica explicita; +- retorno ao Game sem clareza sobre estado pausado, input, render invalidation e retomada da VM. + +Sem esse contrato, a separacao VM/render da `DSC-0040` pode ficar tecnicamente correta, mas o SystemOS ainda pode permitir estados incoerentes. + +## Pontos Criticos + +- Existe apenas um foreground visual owner por vez. +- Background execution e outro assunto e deve ficar fora da primeira decisao, salvo se for necessario definir uma proibicao explicita. +- Shell VM-backed nao deve herdar politica de game loop so por usar VM. +- Game pausado provavelmente deve reter estado, mas nao deve consumir frames logicos. +- O Hub/Shell precisa ser capaz de rodar enquanto o Game esta suspenso. +- A transicao deve invalidar render submissions antigas para impedir present de frames obsoletos. +- O modelo precisa caber no SystemOS/lifecycle existente, nao diretamente no render worker. + +## Opcoes + +### Opcao A - Foreground unico, Game suspenso, Shell unico + +- **Abordagem:** permitir no maximo um Game carregado e no maximo um Shell foreground; ao apertar Home, o Game entra em estado pausado/suspenso e Shell/Hub assume o foreground. Shell VM-backed pode executar apenas enquanto for o foreground Shell. +- **Pro:** modelo simples, previsivel e alinhado com console; reduz concorrencia real entre VMs; combina com a regra de render ownership unico. +- **Contra:** nao cobre multitarefa/background; futuras features de overlay ou apps persistentes precisarao expandir o contrato. +- **Manutenibilidade:** alta; SystemOS pode modelar uma pilha pequena de foreground/suspended sem virar scheduler generico. + +### Opcao B - Multiplas VMs residentes, somente foreground executa + +- **Abordagem:** permitir que Game e Shell VM-backed coexistam como VMs residentes, mas somente a entidade foreground executa. O Game pausado fica residente, sem tick; Shell VM-backed roda sob lifecycle Shell. +- **Pro:** preserva estado e permite retorno rapido; nao exige background execution. +- **Contra:** precisa contrato claro de memoria, ownership de input, foco, cancelamento e fechamento. +- **Manutenibilidade:** boa se os estados forem explicitos; ruim se a residencia for confundida com execucao em background. + +### Opcao C - Encerrar Game ao entrar no Shell VM-backed + +- **Abordagem:** Home fecha/salva o Game antes de abrir Shell VM-backed, evitando coexistencia. +- **Pro:** simplifica o runtime. +- **Contra:** quebra a expectativa de console moderno; torna `Game -> Shell -> Game` um reload, nao uma pausa. +- **Manutenibilidade:** simples no curto prazo, mas provavelmente gera mais excecoes e UX ruim depois. + +### Opcao D - Background real desde ja + +- **Abordagem:** permitir Game pausado ou Shell apps rodando em background com politicas de prioridade. +- **Pro:** cobre casos futuros ricos. +- **Contra:** amplia demais escopo: scheduler, budget, energia, audio, input, IO e memoria. +- **Manutenibilidade:** baixa para v1; mistura a decisao de foreground com multitarefa real. + +## Sugestao / Recomendacao + +Recomendo seguir com a combinacao das opcoes A e B: + +- somente um Game residente por vez; +- somente um Shell foreground por vez; +- Game pode ficar pausado/suspenso enquanto Shell esta foreground; +- Shell VM-backed pode existir junto com Game pausado, mas segue lifecycle Shell; +- somente o owner foreground executa VM/tick/event loop conforme sua politica; +- background execution fica explicitamente fora de escopo; +- render ownership muda junto com foreground owner e deve invalidar submissions antigas via epoch/generation, conforme `DSC-0040`. + +Isso preserva o ciclo `Game -> Home/Shell -> Game` sem transformar o runtime em multitarefa geral agora. + +## Perguntas em Aberto + +- [ ] Qual e o nome canonico dos estados de Game fora do foreground: `Paused`, `Suspended`, ambos, ou outro? +- [ ] Ao apertar Home, o Game deve receber um evento/interrupt/trap de pausa antes de parar de executar? +- [ ] O Hub e um Shell especial sempre presente ou uma task Shell comum? +- [ ] Um app Shell VM-backed pode substituir o Hub no foreground ou o Hub permanece como owner raiz? +- [ ] Quando o Shell VM-backed fecha, o retorno vai sempre para Hub ou pode voltar diretamente ao Game? +- [ ] O input do Game e drenado/limpo ao pausar e ao retomar? +- [ ] Audio do Game pausa junto com a VM ou tem politica propria? +- [ ] Existe limite de memoria/residencia para manter Game pausado enquanto Shell app roda? +- [ ] O retorno ao Game exige novo frame antes de trocar a superficie, ou a tela pode manter Hub/Shell ate a primeira submission valida? +- [ ] Quais eventos de lifecycle precisam ser visiveis para firmware, OS, VM e host? + +## Criterio para Encerrar + +A agenda pode ser encerrada quando tivermos uma decisao clara sobre: + +- cardinalidade de Games e Shells residentes/foreground; +- semantica de Game pausado/suspenso; +- politica de execucao para Shell VM-backed durante Game pausado; +- regra de foreground visual owner e render invalidation; +- comportamento minimo do ciclo `Game -> Shell -> Game`; +- itens explicitamente fora de escopo, especialmente background execution real. + +## Discussao + +- 2026-06-05: Agenda criada a partir da `DSC-0040`, quando o caso de Shell VM-backed aberto sobre um Game pausado mostrou que a discussao pertence ao lifecycle/SystemOS, nao apenas ao render worker. diff --git a/discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md b/discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md new file mode 100644 index 00000000..bd1cf38b --- /dev/null +++ b/discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md @@ -0,0 +1,245 @@ +--- +id: DEC-0031 +ticket: vm-render-parallel-execution-boundary +title: VM and Render Parallel Execution Boundary +status: accepted +created: 2026-06-05 +ref_agenda: AGD-0040 +tags: [runtime, renderer, vm, concurrency, architecture, perf] +--- + +## Status + +Open decision draft generated from accepted agenda `AGD-0040`. + +This decision is ready for review/acceptance. Once accepted, it becomes the normative contract for any spec or code plan that introduces a real render worker/core. + +## Contexto + +`DSC-0038` established `RenderSubmission` as a closed typed snapshot. That gives Prometeu the data boundary needed for future concurrency, but it did not define the operational contract for running VM/logical execution and render consumption on different threads/cores. + +The current runtime consumes submissions synchronously through local `Hardware`/`Gfx` paths. Moving rasterization/presentation out of the VM path requires explicit rules for packet completeness, shared resources, handoff, pacing, lifecycle ownership, and telemetry. + +## Decisao + +Prometeu SHALL treat VM execution and render consumption as separate responsibilities connected by closed render submissions. + +For the first real asynchronous render model, the runtime SHALL use a single-slot latest-wins handoff for Game pipelines, paced by a frame scheduler, with render ownership guarded by epoch/generation and observable through telemetry. Shell/System UI SHALL remain lifecycle/event-driven and local/synchronous by default, while preserving the same closed-packet boundary. + +This decision does not require implementing the worker immediately. It locks the architecture that any future worker implementation MUST follow. + +## Rationale + +The design preserves the console/game timing model without allowing the VM to depend on render success. It also avoids an unbounded queue, stale frame presentation, mutable access to live VM/renderer state, and accidental treatment of Shell UI as a game renderer. + +The decision keeps the first worker model small: + +- the VM publishes at most one latest pending submission; +- the worker consumes owned immutable submissions; +- the frame scheduler, not render ACK, paces logical Game frames; +- lifecycle transitions invalidate stale visual ownership; +- telemetry reports drops/repeats/errors without changing VM semantics. + +## Invariantes / Contrato + +### 1. Packet completo + +`RenderSubmissionPacket::Game2D` MUST be a complete frame description for the render consumer. + +The Game2D consumer MUST compose in this order: + +```text +Game2D composer / scene / layers / sprites +-> gfx2d primitives overlay +-> publication / present +``` + +The active-scene path MUST NOT bypass the packet or drop `gfx2d`. `gfx2d` primitives are a final overlay both with and without an active scene. + +### 2. Recursos compartilhados + +Packets crossing the VM/render boundary MUST remain small and owned. Heavy resources MUST NOT be copied into every submission. + +The packet SHALL carry stable IDs/handles. Large resident resources such as glyph banks, scene banks, assets, and viewport cache materializations SHALL be accessed through read-only APIs. + +The viewport cache belongs to the logical core. The render worker MAY read it through a read-only API after logical/core preparation has completed. Resolver updates, cache refreshes, asset installation, and bank residency changes MUST happen before handoff or in an owning service, not during worker rasterization. + +The system does not guarantee visual integrity when a developer/framework swaps resources behind an in-flight submission in a way that violates asset discipline. Submissions refer to resource IDs; disciplined replacement is the responsibility of the developer/framework layer. + +Any implementation that crosses a Rust thread boundary MUST prove the handoff and read-only resource APIs are safe for sharing. The worker MUST NOT receive `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM/runtime state. + +### 3. Handoff + +The base handoff protocol SHALL be single-slot latest-wins. + +The producer moves an owned closed submission into a pending slot. If the worker has not consumed that pending submission and the producer publishes a newer one, the newer submission replaces the older one. Replacement MUST be counted as a dropped/replaced-before-consume event. + +The worker owns any submission it has taken from the slot. A currently-rendering submission is outside the pending slot. + +The VM/producer MUST NOT block waiting for worker ACK, consumption, raster completion, or present completion. Worker consumed/presented status is telemetry only. + +Shell UI is outside the worker protocol by default. A future configuration MAY reuse the same handoff for Shell, but that is not the default policy. + +### 4. Frame pacing + +Game logical frames SHALL be paced by a `FrameClock`/`FrameScheduler` authority, not by render-worker ACK. + +The VM MUST NOT run freely ahead of display cadence. The target is one logical Game frame per frame tick and at most one pending submission ahead of the worker. The worker presents at display cadence, initially 60Hz where available, and repeats the last valid frame if no new valid submission exists. + +`FRAME_SYNC` remains the canonical end of a VM logical frame. The old budget mechanism MUST NOT be used as the normal way to cut or advance a logical frame. Budget/cycle/time accounting remains for certification, watchdog, diagnostics, and overrun reporting. + +If a logical frame exceeds the display window, logical frames remain sequential. The runtime records overrun/stutter and the worker repeats the last valid frame. The delayed submission represents the next sequential logical frame, not a skipped `N+k` frame. + +Input sampling for Game SHOULD occur at the start of an authorized logical frame. Repeated presents without a new logical frame MUST NOT imply new input sampling or a hidden logical update. + +### 5. Politica por AppMode / pipeline + +Render execution policy SHALL be explicit by AppMode/pipeline. + +Game workloads are frame-paced by definition. Current `AppMode::Game` and future explicit Game2D/Game3D modes SHALL use the frame scheduler and MAY use a render worker by default when the host/runtime supports it, with synchronous/local fallback. + +Shell/System UI is lifecycle/event-driven. It MUST NOT inherit the Game logical frame loop. Shell UI SHALL publish local/synchronous by default and render by invalidation/event/lifecycle. Shell VM-backed apps follow Shell lifecycle and MUST NOT declare an independent frame-paced workload under this decision. + +Firmware/system screens such as splash, crash, and hub follow Shell/local policy unless a later decision creates a more specific policy. + +SystemOS/lifecycle decides the semantic profile/AppMode. `RenderManager` executes the render policy. Host/HAL provides capabilities and concrete execution. + +### 6. Lifecycle, epoch e ownership + +Every submission that may cross an asynchronous boundary MUST carry enough ownership context to reject stale presentation. This includes frame identity plus render ownership such as epoch/generation and owner/app/mode identity as needed. + +Any foreground visual-owner transition SHALL increment/apply a render epoch or equivalent generation through a central `RenderManager` API. SystemOS/lifecycle decides that a semantic transition occurred; `RenderManager` applies the render ownership transition. + +Transitions that MUST invalidate stale pending/current submissions include: + +- Game -> Shell/Hub; +- Shell/Hub -> Game; +- crash screen; +- splash or system screen takeover; +- cartridge/app swap, even if `AppMode` remains the same; +- shutdown/stop. + +The worker MAY finish raster work for an obsolete submission, but it MUST check epoch/owner before present and MUST NOT present frames that no longer match active ownership. + +The same physical surface MAY be shared by Game, Shell, Hub, splash, and crash screens, but logical ownership MUST be explicit. On Shell -> Game, the runtime MAY keep the previous Shell/Hub surface visible until the first valid Game submission; clearing is a visual policy, not a correctness requirement. + +Shutdown MUST stop presentation of obsolete work, discard pending submissions, invalidate current work through epoch/stop token, and terminate/join the worker in a bounded way. It MUST NOT drain obsolete frames to the screen. + +Foreground stack, Game pause/resume, and coexistence of paused Game with VM-backed Shell apps are outside this decision and are tracked separately by `DSC-0041`. + +### 7. Erros, drops e telemetria + +Asynchronous render is observable best-effort, not a semantic handshake with the VM. + +Render drops, repeated presents, stale-epoch discards, raster errors, and present errors MUST be reported through runtime/host telemetry. They MUST NOT directly alter VM program semantics, and VM code MUST NOT depend on "this frame was presented" to advance logic. + +Minimum counters: + +- `produced_submissions`; +- `replaced_before_consume`; +- `consumed_submissions`; +- `presented_frames`; +- `repeated_presents`; +- `render_errors`; +- `present_errors`; +- `stale_epoch_discards`; +- `shutdown_discards`. + +Minimum IDs/state: + +- `last_produced_frame_id`; +- `last_consumed_frame_id`; +- `last_presented_frame_id`; +- `last_dropped_frame_id`; +- `last_error_frame_id`; +- `active_render_epoch`. + +Minimum events: + +- submission replaced in the pending slot before consumption; +- worker consumed a submission; +- worker discarded a submission due to stale epoch/owner; +- worker presented a frame; +- worker repeated the last frame because no new valid submission existed; +- raster error; +- present error; +- shutdown/cancel discarded pending/current work. + +Raster error on a submission SHOULD discard that submission, record telemetry, and preserve the last valid frame/current surface when possible. Present errors SHOULD record telemetry and MAY escalate to host/system fault policy if repeated. Worker panic MUST become controlled worker/backend failure and MUST NOT propagate as a VM panic. + +## Impactos + +### Spec + +Specs need to describe: + +- closed render packet completeness; +- handoff and pacing model; +- render ownership epoch/generation; +- AppMode/pipeline policy; +- minimum telemetry surface. + +### Runtime + +`RenderManager` needs to become the central owner of render handoff, policy execution, epoch/generation, and render telemetry. It remains coordinator, not renderer. + +### Host / HAL + +Host/HAL provides concrete execution capability: synchronous/local render, render worker thread, separate core, DMA/copresenter, or fallback. Host/HAL does not decide lifecycle ownership. + +### Firmware / SystemOS + +SystemOS/lifecycle remains the semantic owner of mode/app/task transitions. Firmware state changes must route visual ownership transitions through the render ownership API when asynchronous render exists. + +### Tests + +Plans derived from this decision must include tests for: + +- active-scene Game2D preserving `gfx2d` overlay after layer composition; +- latest-wins replacement telemetry; +- stale epoch discard before present; +- Shell local/default policy remaining independent from Game pacing; +- logical frame overrun producing repeat/stutter telemetry without frame skip semantics. + +## Alternativas Descartadas + +### Fila crescente de submissions + +Rejected because it increases latency, memory usage, and stale frame risk. The first worker model should not accumulate old frames. + +### Worker como dono do frame counter canonico + +Rejected because it mixes pacing authority with raster/present completion. Frame cadence belongs to the frame scheduler; the worker is a consumer/presenter. + +### Shell sempre no render worker + +Rejected because Shell/System UI is lifecycle/event-driven. Forcing worker usage adds complexity without clear benefit. + +### Capturar recursos pesados por frame + +Rejected because it is expensive and makes resource lifetime policy obscure. Packets should carry IDs and use read-only resident resources. + +### ACK de present como parte da semantica da VM + +Rejected because it couples deterministic VM progress to host/render timing and failure modes. + +## Referencias + +- `AGD-0040`: VM and Render Parallel Execution Boundary. +- `DSC-0038` / `LSN-0047`: typed render submissions preserve domain boundaries. +- `DSC-0031` / `LSN-0040`: AppMode separates Game and System/Shell profiles. +- `DSC-0032` / `LSN-0041`: SystemOS owns lifecycle semantics. +- `DSC-0035` / `LSN-0044`: Shell liveness depends on task-owned focused window. +- `DSC-0036` / `LSN-0045`: Hub UI slices prove lifecycle boundaries. +- `DSC-0041`: foreground stack, Game pause, and VM-backed Shell coexistence. + +## Propagacao Necessaria + +- Create a plan before spec or code execution. +- First plan should probably separate immediate packet correctness from future worker infrastructure. +- Spec text must be in English. +- Lessons must be created only after execution publishes spec/code state. + +## Revision Log + +- 2026-06-05: Initial decision draft generated from accepted `AGD-0040`. diff --git a/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md b/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md new file mode 100644 index 00000000..b9156dc2 --- /dev/null +++ b/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md @@ -0,0 +1,62 @@ +--- +id: PLN-0087 +ticket: vm-render-parallel-execution-boundary +title: Fix Game2D Packet Overlay Composition +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, renderer, game2d, packet, overlay] +--- + +## Briefing + +`DEC-0031` requires `RenderSubmissionPacket::Game2D` to be a complete frame description. The current active-scene path can bypass the packet and lose `gfx2d` overlay commands. + +## Objective + +Make Game2D packet consumption identical for active-scene and no-scene frames: scene/layers/sprites first, then `gfx2d` primitives as final overlay. + +## Dependencies + +- Source decision: `DEC-0031`. +- Existing Game2D packet types in `crates/console/prometeu-hal`. +- Existing publish path in `crates/console/prometeu-drivers`. + +## Scope + +- Update Game2D render publication so active-scene rendering consumes the packet and applies `gfx2d`. +- Keep Shell UI behavior unchanged. +- Add regression tests for active scene plus primitive overlay. + +## Non-Goals + +- Do not introduce a render worker. +- Do not change frame pacing, telemetry, lifecycle, or AppMode policy. +- Do not redesign `FrameComposer` beyond what is needed to consume the packet correctly. + +## Execution Method + +1. Inspect `Hardware::publish_render_submission`, `Gfx::render_game2d_frame_packet`, and `FrameComposer::render_frame`. +2. Replace the active-scene branch that bypasses `Game2DFramePacket` with a packet-consuming path. +3. Ensure `gfx2d` commands are applied after scene/layer/sprite composition in all Game2D branches. +4. Add a test fixture with an active scene and a primitive overlay that would visibly overwrite or mark the composed layer. +5. Add a no-scene regression test if current coverage does not already lock the overlay order. + +## Acceptance Criteria + +- Active-scene Game2D frames render `gfx2d` after layer/sprite composition. +- No-scene Game2D frames preserve the existing overlay behavior. +- `Hardware::publish_render_submission` does not discard `packet.gfx2d`. +- Tests fail on the old active-scene bypass behavior. + +## Tests + +- Unit or integration test around Game2D packet publication. +- Pixel/assertion test for overlay-after-scene ordering where practical. +- Existing renderer tests continue to pass. + +## Affected Artifacts + +- `crates/console/prometeu-drivers/src/hardware.rs` +- `crates/console/prometeu-hal/src/render_submission.rs` +- `crates/console/prometeu-system` or renderer test modules as needed. diff --git a/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md b/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md new file mode 100644 index 00000000..5f0f61c9 --- /dev/null +++ b/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md @@ -0,0 +1,63 @@ +--- +id: PLN-0088 +ticket: vm-render-parallel-execution-boundary +title: Define Read-Only Render Resource Boundary +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, renderer, resources, cache, banks] +--- + +## Briefing + +`DEC-0031` requires render packets to carry small owned data and stable resource IDs, while heavy banks/caches remain resident and read-only to render consumers. + +## Objective + +Define and enforce the read-only resource boundary needed before any worker can safely read scene/glyph/cache data. + +## Dependencies + +- Source decision: `DEC-0031`. +- `PLN-0087` should clarify the packet consumer path before this plan finalizes worker-facing APIs. + +## Scope + +- Identify every resource read during Game2D rendering. +- Classify data as packet-owned, resident read-only, or logical-core mutable preparation state. +- Add trait/API boundaries for render read access where missing. +- Document which types must be `Send + Sync` for a real threaded worker. + +## Non-Goals + +- Do not make resource swaps transactional. +- Do not capture `Arc` or `Arc` inside every submission. +- Do not implement background asset loading policy. + +## Execution Method + +1. Trace Game2D render reads from packet to glyph bank, scene bank, viewport cache, and framebuffer helpers. +2. Create a short internal module-level contract for render resource access. +3. Add or refine read-only access traits for banks/cache without exposing mutable runtime state. +4. Add compile-time assertions or targeted tests for `Send + Sync` where a threaded worker will require them. +5. Keep resource identity in submissions as IDs/handles, not heavy ownership captures. + +## Acceptance Criteria + +- Render consumer APIs can be described as packet-owned plus read-only resource access. +- No worker-facing API requires `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state. +- Shared read APIs that cross thread boundaries have explicit `Send + Sync` expectations. +- Existing synchronous renderer behavior remains unchanged. + +## Tests + +- Compile-time trait assertions for read-only resource APIs where practical. +- Unit tests for read-only bank/cache access. +- Existing asset/scene rendering tests continue to pass. + +## Affected Artifacts + +- `crates/console/prometeu-hal` +- `crates/console/prometeu-system` +- `crates/console/prometeu-drivers` +- Relevant renderer/resource tests. diff --git a/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md b/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md new file mode 100644 index 00000000..71fe7ba6 --- /dev/null +++ b/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md @@ -0,0 +1,62 @@ +--- +id: PLN-0089 +ticket: vm-render-parallel-execution-boundary +title: Introduce Render Handoff Abstraction +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, renderer, handoff, latest-wins] +--- + +## Briefing + +`DEC-0031` requires a single-slot latest-wins handoff where the producer never blocks on render ACK and replacement before consumption is observable. + +## Objective + +Introduce a render handoff abstraction that can be used synchronously today and by a worker later. + +## Dependencies + +- Source decision: `DEC-0031`. +- `PLN-0093` should define telemetry counters before final instrumentation is wired. + +## Scope + +- Add a small handoff type or trait around one pending owned `RenderSubmission`. +- Support publish, take latest, replace pending, and drain/discard operations. +- Report replacement/drop events to telemetry hooks. +- Integrate the abstraction into `RenderManager` without starting a worker. + +## Non-Goals + +- Do not spawn a thread. +- Do not implement frame pacing. +- Do not change Shell UI default rendering policy. + +## Execution Method + +1. Locate `RenderManager::latest_complete_submission` ownership and publication flow. +2. Introduce a handoff module with single pending slot semantics. +3. Move `RenderSubmission` into the slot on publication. +4. Make `take_latest` transfer ownership to the consumer. +5. Count replacement when a pending submission is overwritten before consumption. +6. Keep current synchronous publication working by immediately taking and rendering through the same abstraction. + +## Acceptance Criteria + +- `RenderManager` no longer relies on an unconstrained raw `Option` for the future worker boundary. +- Publishing a newer pending submission replaces the old one without blocking. +- The consumer receives owned submissions. +- Replacement before consume is observable. + +## Tests + +- Unit tests for empty take, publish/take, publish/replace/take, and drain/discard. +- Integration smoke test that existing synchronous render still presents frames. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs` +- New or existing render handoff module under `crates/console/prometeu-system`. +- Render manager tests. diff --git a/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md b/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md new file mode 100644 index 00000000..d097ee40 --- /dev/null +++ b/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md @@ -0,0 +1,63 @@ +--- +id: PLN-0090 +ticket: vm-render-parallel-execution-boundary +title: Add Render Frame Pacing Contract +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, frame-clock, scheduler, vm] +--- + +## Briefing + +`DEC-0031` separates Game logical frame pacing from render worker ACK. Game frames are paced by a frame clock/scheduler and still end at `FRAME_SYNC`. + +## Objective + +Define and implement the runtime contract that authorizes Game logical frames without using render present completion as ACK. + +## Dependencies + +- Source decision: `DEC-0031`. +- `PLN-0091` should define which AppModes consume this policy. + +## Scope + +- Introduce or formalize a `FrameClock`/`FrameScheduler` authority for Game workloads. +- Ensure Game VM runs at most one logical frame per authorized frame tick. +- Preserve `FRAME_SYNC` as logical frame end. +- Record overrun/repeat conditions through telemetry hooks. + +## Non-Goals + +- Do not make Shell UI run a game loop. +- Do not implement catch-up frames or logical frame skipping. +- Do not tie VM progress to render ACK. + +## Execution Method + +1. Inspect current tick/budget/`FRAME_SYNC` handling. +2. Identify where a frame tick should authorize Game VM execution. +3. Introduce a narrow scheduling API consumed by Game lifecycle code. +4. Preserve budget/cycle/time accounting only for watchdog, certification, and diagnostics. +5. Add telemetry points for logical-frame-overrun and no-new-frame/repeat. +6. Document the distinction between logical frame ID, produced submission ID, and presented frame ID. + +## Acceptance Criteria + +- Game VM cannot run unlimited logical frames ahead of the frame clock. +- Render worker or render consumer ACK is not required to start the next logical frame. +- Slow logical frames remain sequential and produce overrun/stutter telemetry. +- Shell lifecycle remains unaffected. + +## Tests + +- Unit tests for scheduler permitting one Game frame per tick. +- Test that no catch-up `N+k` logical frame is produced after overrun. +- Regression tests around `FRAME_SYNC` behavior. + +## Affected Artifacts + +- VM runtime scheduling modules. +- Firmware/Game running step. +- Telemetry interfaces touched by pacing. diff --git a/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md b/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md new file mode 100644 index 00000000..94be136b --- /dev/null +++ b/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md @@ -0,0 +1,63 @@ +--- +id: PLN-0091 +ticket: vm-render-parallel-execution-boundary +title: Implement AppMode Render Policy Matrix +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, appmode, shell, game, policy] +--- + +## Briefing + +`DEC-0031` requires render execution policy to be explicit by AppMode/pipeline: Game is frame-paced and may use worker; Shell/System UI is lifecycle-driven and local/synchronous by default. + +## Objective + +Add an explicit render policy matrix so Game and Shell rendering cannot accidentally share the wrong lifecycle or pacing model. + +## Dependencies + +- Source decision: `DEC-0031`. +- `PLN-0090` defines the Game pacing contract. + +## Scope + +- Represent render policy for current `AppMode::Game` and `AppMode::Shell`. +- Make policy selection a SystemOS/lifecycle decision executed by `RenderManager`. +- Add host/runtime capability fallback for worker-supported vs local render. +- Keep Shell VM-backed apps under Shell lifecycle. + +## Non-Goals + +- Do not rename `AppMode::Game` to `Game2D` in this plan. +- Do not implement Game3D. +- Do not define Game pause/resume foreground stack; that belongs to `DSC-0041`. + +## Execution Method + +1. Locate `AppMode` routing through cartridge load, firmware state, SystemOS, and `RenderManager`. +2. Add a render policy enum or equivalent explicit matrix. +3. Route Game to frame-paced policy with optional worker capability. +4. Route Shell/system screens to local/synchronous lifecycle policy. +5. Add assertions/tests that Shell UI does not consume Game frame scheduler. + +## Acceptance Criteria + +- Policy is explicit and testable for Game and Shell. +- Shell UI remains local/synchronous by default. +- Game can declare worker-capable policy without requiring the worker to exist. +- Host/runtime capability fallback is represented without moving ownership decisions to host. + +## Tests + +- Unit tests for AppMode-to-render-policy mapping. +- Lifecycle test proving Shell does not require Game frame ticks. +- Regression test for Game still producing frame-paced submissions. + +## Affected Artifacts + +- `AppMode` routing modules. +- `RenderManager`. +- Firmware load/run steps. +- SystemOS lifecycle integration tests. diff --git a/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md b/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md new file mode 100644 index 00000000..4746f5fc --- /dev/null +++ b/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md @@ -0,0 +1,64 @@ +--- +id: PLN-0092 +ticket: vm-render-parallel-execution-boundary +title: Add Render Epoch Ownership Transitions +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, lifecycle, render-epoch, ownership] +--- + +## Briefing + +`DEC-0031` requires foreground visual-owner transitions to invalidate stale render work using epoch/generation and owner identity. + +## Objective + +Add render epoch/ownership metadata and transition APIs so stale submissions cannot be presented after mode, app, crash, cartridge, or shutdown transitions. + +## Dependencies + +- Source decision: `DEC-0031`. +- `PLN-0089` provides the handoff abstraction. + +## Scope + +- Add epoch/generation and owner identity to render submission metadata. +- Add central `RenderManager` API for applying ownership transitions. +- Integrate lifecycle transitions that change visual owner. +- Reject stale pending/current submissions before present. + +## Non-Goals + +- Do not implement smooth visual transitions. +- Do not define Game pause/resume stack semantics from `DSC-0041`. +- Do not give host/HAL authority to decide ownership. + +## Execution Method + +1. Define render owner metadata shape: epoch/generation plus mode/app/surface owner as needed. +2. Extend `RenderSubmission` metadata and constructors. +3. Add `RenderManager` transition API that increments/applies epoch. +4. Call the API from SystemOS/firmware lifecycle transition points. +5. Update render consumption to check ownership before present. +6. Add discard telemetry hook for stale epoch. + +## Acceptance Criteria + +- Game -> Shell, Shell -> Game, crash, splash, cartridge/app swap, and shutdown can invalidate stale render work. +- Worker/current consumer cannot present a submission with stale epoch/owner. +- Host/HAL does not decide semantic ownership. +- Same physical surface can be reused with distinct logical ownership. + +## Tests + +- Unit test stale epoch discard before present. +- Integration test mode transition with pending Game submission. +- Test cartridge/app swap increments ownership even when AppMode remains unchanged. + +## Affected Artifacts + +- `RenderSubmission` metadata. +- `RenderManager`. +- Firmware/SystemOS transition code. +- Render publication path. diff --git a/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md b/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md new file mode 100644 index 00000000..54dba281 --- /dev/null +++ b/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md @@ -0,0 +1,66 @@ +--- +id: PLN-0093 +ticket: vm-render-parallel-execution-boundary +title: Add Render Telemetry Counters and Events +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, renderer, telemetry, diagnostics] +--- + +## Briefing + +`DEC-0031` requires render drops, repeats, stale discards, render errors, and present errors to be observable without becoming VM semantics. + +## Objective + +Add the minimum render telemetry surface required for asynchronous render diagnostics and certification. + +## Dependencies + +- Source decision: `DEC-0031`. +- `PLN-0089` and `PLN-0092` provide main event sources. + +## Scope + +- Add minimum counters and last-frame IDs from `DEC-0031`. +- Add event recording hooks for handoff, consume, present, repeat, stale discard, errors, and shutdown discard. +- Expose telemetry through existing runtime/host diagnostics patterns. +- Ensure VM program behavior cannot observe telemetry as render ACK. + +## Non-Goals + +- Do not build a UI profiler. +- Do not add VM syscalls for present ACK. +- Do not define host-specific logging formats beyond necessary hooks. + +## Execution Method + +1. Locate current telemetry facilities and atomic counter patterns. +2. Add render telemetry structure with counters and last IDs. +3. Wire replacement-before-consume from handoff. +4. Wire consumed/presented/repeated from consumer path. +5. Wire stale epoch and shutdown discards. +6. Wire render/present error reporting. +7. Add read-only diagnostics access for host/debug tooling. + +## Acceptance Criteria + +- All minimum counters and IDs from `DEC-0031` exist. +- Latest-wins replacement is measurable. +- Repeated presents and stale epoch discards are measurable. +- Telemetry is not a blocking ACK path for the VM. + +## Tests + +- Unit tests for counter increments. +- Handoff replacement telemetry test. +- Stale epoch discard telemetry test. +- Error path telemetry test where render/present errors can be simulated. + +## Affected Artifacts + +- Runtime telemetry modules. +- `RenderManager`. +- Render handoff and publication paths. +- Host/debug telemetry readers. diff --git a/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md b/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md new file mode 100644 index 00000000..71411357 --- /dev/null +++ b/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md @@ -0,0 +1,60 @@ +--- +id: PLN-0094 +ticket: vm-render-parallel-execution-boundary +title: Specify Async Render Boundary +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [spec, runtime, renderer, architecture] +--- + +## Briefing + +`DEC-0031` needs canonical English spec coverage before worker-oriented code becomes normative. + +## Objective + +Publish the async render boundary contract in the canonical specs location. + +## Dependencies + +- Source decision: `DEC-0031`. +- Should run before large worker implementation plans are executed. + +## Scope + +- Add or update English spec text for packet completeness, handoff, pacing, AppMode policy, epoch ownership, and telemetry. +- Link the spec language to runtime concepts without exposing implementation-only details. +- Keep `DSC-0041` foreground stack details out of this spec pass except as explicit out-of-scope reference. + +## Non-Goals + +- Do not document future Game3D behavior beyond extensibility notes. +- Do not write lessons; lessons happen after execution. +- Do not encode implementation file paths as spec requirements. + +## Execution Method + +1. Locate canonical specs for runtime/render/AppMode. +2. Add a section for asynchronous render boundary or update the closest existing spec. +3. Translate normative parts of `DEC-0031` into concise English spec language. +4. Add out-of-scope note for Game pause/resume and VM-backed Shell coexistence under `DSC-0041`. +5. Cross-check terminology against existing specs. + +## Acceptance Criteria + +- Canonical specs describe the async render boundary in English. +- Spec language covers all seven decision areas. +- No Portuguese text is introduced in specs. +- The spec does not require worker implementation before the plan for it exists. + +## Tests + +- Documentation review. +- `rg` check for accidental Portuguese in touched spec files. +- Existing doc/spec validation scripts if present. + +## Affected Artifacts + +- Canonical specs location in the repository. +- `discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md` as reference only. diff --git a/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md b/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md new file mode 100644 index 00000000..48a67003 --- /dev/null +++ b/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md @@ -0,0 +1,66 @@ +--- +id: PLN-0095 +ticket: vm-render-parallel-execution-boundary +title: Prototype Local Render Worker Capability +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, host, renderer, worker, prototype] +--- + +## Briefing + +`DEC-0031` allows Game pipelines to use a render worker when host/runtime capabilities support it, with local/synchronous fallback. + +## Objective + +Prototype the first local render worker capability behind the established handoff, policy, epoch, and telemetry contracts. + +## Dependencies + +- `PLN-0087` through `PLN-0093` should be complete or accepted as prerequisites. +- `PLN-0094` should publish the spec boundary before this becomes production work. + +## Scope + +- Add a host/runtime capability flag for render worker support. +- Spawn a local worker for Game policy only. +- Consume the single-slot handoff. +- Present at display cadence or the nearest available host cadence. +- Preserve synchronous/local fallback. + +## Non-Goals + +- Do not optimize performance beyond proving the contract. +- Do not enable Shell worker by default. +- Do not implement hardware-specific core/DMA behavior. + +## Execution Method + +1. Add capability wiring for worker-supported vs local render. +2. Create worker lifecycle owned by runtime/host integration according to `RenderManager` policy. +3. Make worker consume owned submissions through the handoff. +4. Check epoch/owner before present. +5. Emit telemetry for consume, present, repeat, stale discard, and errors. +6. Keep a build/runtime path that uses the existing synchronous renderer. + +## Acceptance Criteria + +- Game rendering can run through a local worker in a controlled configuration. +- Shell rendering remains local/synchronous by default. +- Worker does not access live mutable VM, `Hardware`, `Gfx`, or `FrameComposer` state outside approved boundaries. +- Fallback to synchronous/local render remains available. + +## Tests + +- Worker smoke test with Game submission. +- Fallback mode smoke test. +- Stale epoch discard test while worker is active. +- Thread shutdown test covered with `PLN-0096`. + +## Affected Artifacts + +- Host desktop runtime integration. +- `RenderManager`. +- Render handoff. +- Render backend/driver integration points. diff --git a/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md b/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md new file mode 100644 index 00000000..03cd52db --- /dev/null +++ b/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md @@ -0,0 +1,70 @@ +--- +id: PLN-0096 +ticket: vm-render-parallel-execution-boundary +title: Define Render Shutdown and Failure Handling +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, renderer, shutdown, errors] +--- + +## Briefing + +`DEC-0031` requires shutdown to stop obsolete presentation, discard pending/current work, and terminate worker execution in a bounded way. Worker panic must become controlled failure, not VM panic. + +## Objective + +Define and implement render shutdown, cancellation, and failure handling for both synchronous and worker-capable paths. + +## Dependencies + +- `PLN-0089` handoff abstraction. +- `PLN-0092` epoch/ownership transitions. +- `PLN-0093` telemetry. +- `PLN-0095` if a real worker is being exercised. + +## Scope + +- Add stop token or equivalent cancellation state for worker/current render work. +- Discard pending submissions on shutdown. +- Prevent obsolete current work from presenting after shutdown/owner change. +- Convert worker panic/render backend failure into controlled runtime/host error state. +- Record shutdown discards and errors in telemetry. + +## Non-Goals + +- Do not design full crash UI policy beyond render ownership requirements. +- Do not implement automatic worker restart unless existing host policy already supports it. +- Do not expose failure ACK to VM program semantics. + +## Execution Method + +1. Define shutdown state machine for render consumer. +2. Add bounded worker termination behavior where worker exists. +3. Add pending/current discard paths. +4. Ensure present path checks stop/epoch before publishing. +5. Wrap worker entrypoint with panic/error containment. +6. Add telemetry for shutdown discards, render errors, present errors, and worker failure. + +## Acceptance Criteria + +- Shutdown does not drain obsolete frames to screen. +- Pending work is discarded. +- Current work cannot present after stop/epoch invalidation. +- Worker failure is reported without panicking the VM. +- Shutdown completes in bounded time. + +## Tests + +- Unit test pending discard on shutdown. +- Integration test shutdown during currently-rendering submission. +- Simulated worker panic/error test. +- Telemetry assertions for shutdown/error counters. + +## Affected Artifacts + +- Render handoff. +- Worker runtime integration. +- `RenderManager`. +- Telemetry modules. +- Host error handling. diff --git a/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md b/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md new file mode 100644 index 00000000..baecaef0 --- /dev/null +++ b/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md @@ -0,0 +1,61 @@ +--- +id: PLN-0097 +ticket: vm-render-parallel-execution-boundary +title: Add Async Render Integration Test Matrix +status: open +created: 2026-06-05 +ref_decisions: [DEC-0031] +tags: [runtime, renderer, tests, integration] +--- + +## Briefing + +`DEC-0031` spans packet correctness, resource boundaries, handoff, pacing, AppMode policy, epoch ownership, telemetry, and failure behavior. These need integration coverage across plans. + +## Objective + +Create an integration test matrix that proves the async render boundary contracts work together and prevents regressions while plans are implemented incrementally. + +## Dependencies + +- Source decision: `DEC-0031`. +- This plan can start early as test scaffolding and expand as other plans land. + +## Scope + +- Define a test matrix mapping each `DEC-0031` invariant to at least one test. +- Add reusable fixtures for Game2D submissions, Shell UI submissions, stale epoch, handoff replacement, and frame repeat. +- Ensure tests run in local/synchronous mode and, when available, worker mode. + +## Non-Goals + +- Do not require real hardware backend. +- Do not require visual screenshot testing unless the existing test stack already supports it. +- Do not test `DSC-0041` foreground stack semantics. + +## Execution Method + +1. Build a matrix with rows for each decision invariant. +2. Identify existing tests that already cover rows. +3. Add missing tests as focused runtime/renderer integration tests. +4. Add helpers to construct deterministic submissions and resource IDs. +5. Run matrix in synchronous mode first. +6. Add worker-mode variants when `PLN-0095` exists. + +## Acceptance Criteria + +- Each `DEC-0031` invariant has at least one automated or explicitly documented validation. +- Test fixtures distinguish Game and Shell policies. +- Tests cover stale epoch discard, latest-wins replacement, repeated present, render error telemetry, and active-scene overlay. +- The matrix is referenced by later implementation plans. + +## Tests + +- This plan is itself test-focused. +- Required categories: packet composition, handoff, pacing, AppMode policy, epoch, telemetry, shutdown/failure. + +## Affected Artifacts + +- Runtime/renderer integration tests. +- Test fixtures/helpers. +- Any existing CI test grouping that needs to include the new tests. From 51f6a36b1dc524e9c03c715cc56e8d90ac3b3d84 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:33:22 +0100 Subject: [PATCH 03/47] implements PLN-0087 --- crates/console/prometeu-drivers/src/gfx.rs | 10 ++++-- .../console/prometeu-drivers/src/hardware.rs | 34 +++++++++++++++++-- discussion/index.ndjson | 2 +- ...7-fix-game2d-packet-overlay-composition.md | 2 +- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 958e1806..6b86cee6 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -661,9 +661,7 @@ impl Gfx { self.load_frame_sprites(&sprites); self.render_no_scene_frame(); - for command in &packet.gfx2d { - self.apply_gfx2d_command(command); - } + self.apply_gfx2d_commands(&packet.gfx2d); } pub fn render_shell_ui_frame_packet(&mut self, packet: &ShellUiFramePacket) { @@ -672,6 +670,12 @@ impl Gfx { } } + pub fn apply_gfx2d_commands(&mut self, commands: &[Gfx2dCommand]) { + for command in commands { + self.apply_gfx2d_command(command); + } + } + fn apply_gfx2d_command(&mut self, command: &Gfx2dCommand) { match command { Gfx2dCommand::Clear { color } => self.clear(*color), diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 1cdaa9c9..77ff7dd8 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -79,6 +79,7 @@ impl HardwareBridge for Hardware { self.gfx.render_game2d_frame_packet(packet); } else { self.frame_composer.render_frame(&mut self.gfx); + self.gfx.apply_gfx2d_commands(&packet.gfx2d); } } RenderSubmissionPacket::ShellUi(packet) => { @@ -187,12 +188,14 @@ mod tests { use prometeu_hal::color::Color; use prometeu_hal::glyph::Glyph; use prometeu_hal::glyph_bank::{GlyphBank, TileSize}; + use prometeu_hal::primitives::Rect; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::scene_viewport_cache::SceneViewportCache; use prometeu_hal::scene_viewport_resolver::SceneViewportResolver; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; + use prometeu_hal::{FrameId, Gfx2dCommand}; fn make_glyph_bank() -> GlyphBank { let mut bank = GlyphBank::new(TileSize::Size8, 8, 8); @@ -204,9 +207,13 @@ mod tests { } fn make_scene() -> SceneBank { + make_scene_with_glyph_asset_id(1) + } + + fn make_scene_with_glyph_asset_id(glyph_asset_id: i32) -> SceneBank { let layer = SceneLayer { active: true, - glyph_asset_id: 1, + glyph_asset_id, tile_size: TileSize::Size8, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { @@ -231,7 +238,7 @@ mod tests { fn hardware_can_render_scene_from_shared_scene_bank_pipeline() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank(0, Arc::new(make_glyph_bank())); - banks.install_scene_bank(0, Arc::new(make_scene())); + banks.install_scene_bank(0, Arc::new(make_scene_with_glyph_asset_id(0))); let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks)); let scene = banks.scene_bank_slot(0).expect("scene bank slot 0 should be resident"); @@ -262,4 +269,27 @@ mod tests { assert_eq!(hardware.frame_composer.scene_bank_slot_count(), 16); assert_eq!(scene.layers[0].tile_size, TileSize::Size8); } + + #[test] + fn publish_game2d_submission_applies_gfx2d_overlay_after_active_scene() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(make_glyph_bank())); + banks.install_scene_bank(0, Arc::new(make_scene_with_glyph_asset_id(0))); + + let mut hardware = Hardware::new_with_memory_banks(banks); + let mut glyph_slots = [None; 16]; + glyph_slots[0] = Some(0); + hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots); + assert!(hardware.bind_scene(0)); + + let mut packet = hardware.close_game2d_packet(); + packet.gfx2d = vec![Gfx2dCommand::FillRect { + rect: Rect { x: 0, y: 0, w: 1, h: 1 }, + color: Color::BLUE, + }]; + + hardware.publish_render_submission(&RenderSubmission::game2d(FrameId::ZERO, packet)); + + assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index e78d68b9..c67119ee 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md b/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md index b9156dc2..a01d9f07 100644 --- a/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md +++ b/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md @@ -2,7 +2,7 @@ id: PLN-0087 ticket: vm-render-parallel-execution-boundary title: Fix Game2D Packet Overlay Composition -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, renderer, game2d, packet, overlay] From b06462111a573c7a7fdd4ab02a9eb606302ca213 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:34:51 +0100 Subject: [PATCH 04/47] implements PLN-0088 --- crates/console/prometeu-drivers/src/lib.rs | 4 +- .../prometeu-drivers/src/memory_banks.rs | 82 +++++++++++++++++++ discussion/index.ndjson | 2 +- ...fine-read-only-render-resource-boundary.md | 2 +- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/crates/console/prometeu-drivers/src/lib.rs b/crates/console/prometeu-drivers/src/lib.rs index c0e4b650..d5173830 100644 --- a/crates/console/prometeu-drivers/src/lib.rs +++ b/crates/console/prometeu-drivers/src/lib.rs @@ -12,7 +12,7 @@ pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE pub use crate::frame_composer::{ComposerBuffer, FrameComposer, SceneStatus, SpriteController}; pub use crate::gfx::Gfx; pub use crate::memory_banks::{ - GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess, - SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller, + GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, RenderResourceAccess, + SceneBankPoolAccess, SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller, }; pub use crate::pad::Pad; diff --git a/crates/console/prometeu-drivers/src/memory_banks.rs b/crates/console/prometeu-drivers/src/memory_banks.rs index bdfd57a3..5cc1bc9c 100644 --- a/crates/console/prometeu-drivers/src/memory_banks.rs +++ b/crates/console/prometeu-drivers/src/memory_banks.rs @@ -1,3 +1,11 @@ +//! Memory bank access boundary. +//! +//! Render consumers must cross this boundary through small, read-only traits. +//! Frame packets carry resource IDs; large resident resources stay in banks and +//! are returned as `Arc` snapshots of the slot contents. Installation and slot +//! replacement remain separate owner-side operations and are not part of the +//! render read surface. + use prometeu_hal::glyph_bank::GlyphBank; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::sound_bank::SoundBank; @@ -45,6 +53,15 @@ pub trait SceneBankPoolInstaller: Send + Sync { fn install_scene_bank(&self, slot: usize, bank: Arc); } +/// Read-only resource surface needed by render consumers. +/// +/// This is intentionally narrower than `MemoryBanks`: render code can resolve +/// stable packet IDs to resident glyph/scene banks, but cannot install or swap +/// resources through this trait. +pub trait RenderResourceAccess: GlyphBankPoolAccess + SceneBankPoolAccess {} + +impl RenderResourceAccess for T where T: GlyphBankPoolAccess + SceneBankPoolAccess {} + /// Centralized container for all hardware memory banks. /// /// MemoryBanks represent the actual hardware slot state. @@ -132,3 +149,68 @@ impl SceneBankPoolInstaller for MemoryBanks { } } } + +#[cfg(test)] +mod tests { + use super::*; + use prometeu_hal::color::Color; + use prometeu_hal::glyph::Glyph; + use prometeu_hal::glyph_bank::TileSize; + use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; + use prometeu_hal::tile::Tile; + use prometeu_hal::tilemap::TileMap; + + fn assert_send_sync() {} + + fn make_glyph_bank() -> GlyphBank { + let mut bank = GlyphBank::new(TileSize::Size8, 8, 8); + bank.palettes[0][1] = Color::WHITE; + bank + } + + fn make_scene_bank() -> SceneBank { + let layer = SceneLayer { + active: true, + glyph_asset_id: 0, + tile_size: TileSize::Size8, + parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, + tilemap: TileMap { + width: 1, + height: 1, + tiles: vec![Tile { + active: true, + glyph: Glyph { glyph_id: 0, palette_id: 0 }, + flip_x: false, + flip_y: false, + }], + }, + }; + SceneBank { layers: std::array::from_fn(|_| layer.clone()) } + } + + #[test] + fn render_resource_access_traits_are_thread_safe() { + assert_send_sync::(); + assert_send_sync::>(); + assert_send_sync::>(); + assert_send_sync::>(); + } + + #[test] + fn render_resource_access_exposes_read_only_glyph_and_scene_banks() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(make_glyph_bank())); + banks.install_scene_bank(1, Arc::new(make_scene_bank())); + + let render_resources: Arc = banks; + let glyph_bank = + render_resources.glyph_bank_slot(0).expect("glyph bank slot should be readable"); + let scene_bank = + render_resources.scene_bank_slot(1).expect("scene bank slot should be readable"); + + assert_eq!(glyph_bank.tile_size, TileSize::Size8); + assert_eq!(scene_bank.layers[0].tile_size, TileSize::Size8); + assert!(render_resources.glyph_bank_slot(15).is_none()); + assert!(render_resources.scene_bank_slot(15).is_none()); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index c67119ee..dc024b20 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md b/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md index 5f0f61c9..0c7098e9 100644 --- a/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md +++ b/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md @@ -2,7 +2,7 @@ id: PLN-0088 ticket: vm-render-parallel-execution-boundary title: Define Read-Only Render Resource Boundary -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, renderer, resources, cache, banks] From d2a53d0efab1db5d36d197c3c3cf0ff0c4f87cda Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:36:55 +0100 Subject: [PATCH 05/47] implements PLN-0089 --- .../src/services/vm_runtime/render_manager.rs | 133 ++++++++++++++++-- discussion/index.ndjson | 2 +- ...89-introduce-render-handoff-abstraction.md | 2 +- 3 files changed, 127 insertions(+), 10 deletions(-) diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs index 10d24bdc..453b63f3 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs @@ -18,11 +18,59 @@ pub trait RenderSurface { fn consume_submission(&mut self, submission: &RenderSubmission); } +#[derive(Debug, Clone, Default)] +pub struct RenderHandoff { + pending: Option, + last_consumed: Option, + replaced_before_consume: u64, + discarded_pending: u64, +} + +impl RenderHandoff { + pub fn publish(&mut self, submission: RenderSubmission) { + if self.pending.replace(submission).is_some() { + self.replaced_before_consume = self.replaced_before_consume.wrapping_add(1); + } + } + + pub fn pending_submission(&self) -> Option<&RenderSubmission> { + self.pending.as_ref() + } + + pub fn latest_submission(&self) -> Option<&RenderSubmission> { + self.pending.as_ref().or(self.last_consumed.as_ref()) + } + + pub fn take_latest(&mut self) -> Option { + self.pending.take() + } + + pub fn record_consumed(&mut self, submission: RenderSubmission) { + self.last_consumed = Some(submission); + } + + pub fn discard_pending(&mut self) -> Option { + let discarded = self.pending.take(); + if discarded.is_some() { + self.discarded_pending = self.discarded_pending.wrapping_add(1); + } + discarded + } + + pub fn replaced_before_consume(&self) -> u64 { + self.replaced_before_consume + } + + pub fn discarded_pending(&self) -> u64 { + self.discarded_pending + } +} + #[derive(Debug, Clone)] pub struct RenderManager { active_app_mode: AppMode, next_frame_id: FrameId, - latest_complete_submission: Option, + handoff: RenderHandoff, transition_state: RenderTransitionState, } @@ -31,7 +79,7 @@ impl RenderManager { Self { active_app_mode, next_frame_id: FrameId::ZERO, - latest_complete_submission: None, + handoff: RenderHandoff::default(), transition_state: RenderTransitionState::Idle, } } @@ -45,7 +93,23 @@ impl RenderManager { } pub fn latest_complete_submission(&self) -> Option<&RenderSubmission> { - self.latest_complete_submission.as_ref() + self.handoff.latest_submission() + } + + pub fn pending_submission(&self) -> Option<&RenderSubmission> { + self.handoff.pending_submission() + } + + pub fn replaced_before_consume(&self) -> u64 { + self.handoff.replaced_before_consume() + } + + pub fn discarded_pending(&self) -> u64 { + self.handoff.discarded_pending() + } + + pub fn discard_pending_submission(&mut self) -> Option { + self.handoff.discard_pending() } pub fn set_active_app_mode(&mut self, app_mode: AppMode) { @@ -80,8 +144,8 @@ impl RenderManager { }; self.next_frame_id = self.next_frame_id.next(); - self.latest_complete_submission = Some(submission); - Ok(self.latest_complete_submission.as_ref().expect("submission was just stored")) + self.handoff.publish(submission); + Ok(self.handoff.pending_submission().expect("submission was just stored")) } pub fn close_compat_frame(&mut self) -> &RenderSubmission { @@ -93,11 +157,12 @@ impl RenderManager { self.close_frame_with_packet(packet).expect("compat packet matches active app mode") } - pub fn publish_latest(&self, surface: &mut S) -> bool { - let Some(submission) = self.latest_complete_submission.as_ref() else { + pub fn publish_latest(&mut self, surface: &mut S) -> bool { + let Some(submission) = self.handoff.take_latest() else { return false; }; - surface.consume_submission(submission); + surface.consume_submission(&submission); + self.handoff.record_consumed(submission); true } } @@ -185,4 +250,56 @@ mod tests { assert_eq!(surface.seen, vec![FrameId::new(0)]); } + + #[test] + fn render_handoff_takes_owned_latest_submission() { + let mut handoff = RenderHandoff::default(); + + assert!(handoff.take_latest().is_none()); + handoff.publish(RenderSubmission::game2d(FrameId::new(7), Game2DFramePacket::default())); + + let submission = handoff.take_latest().expect("pending submission"); + assert_eq!(submission.frame_id, FrameId::new(7)); + assert!(handoff.take_latest().is_none()); + } + + #[test] + fn render_handoff_counts_replacement_before_consume() { + let mut handoff = RenderHandoff::default(); + + handoff.publish(RenderSubmission::game2d(FrameId::new(1), Game2DFramePacket::default())); + handoff.publish(RenderSubmission::game2d(FrameId::new(2), Game2DFramePacket::default())); + + assert_eq!(handoff.replaced_before_consume(), 1); + assert_eq!(handoff.take_latest().expect("latest").frame_id, FrameId::new(2)); + } + + #[test] + fn render_handoff_discards_pending_submission() { + let mut handoff = RenderHandoff::default(); + + assert!(handoff.discard_pending().is_none()); + handoff.publish(RenderSubmission::game2d(FrameId::new(3), Game2DFramePacket::default())); + + let discarded = handoff.discard_pending().expect("discarded pending"); + assert_eq!(discarded.frame_id, FrameId::new(3)); + assert_eq!(handoff.discarded_pending(), 1); + assert!(handoff.take_latest().is_none()); + } + + #[test] + fn publish_latest_consumes_pending_but_keeps_observable_last_submission() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + + manager.close_compat_frame(); + assert!(manager.pending_submission().is_some()); + assert!(manager.publish_latest(&mut surface)); + + assert!(manager.pending_submission().is_none()); + assert_eq!( + manager.latest_complete_submission().expect("last consumed").frame_id, + FrameId::ZERO + ); + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index dc024b20..8152b547 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md b/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md index 71fe7ba6..760139fc 100644 --- a/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md +++ b/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md @@ -2,7 +2,7 @@ id: PLN-0089 ticket: vm-render-parallel-execution-boundary title: Introduce Render Handoff Abstraction -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, renderer, handoff, latest-wins] From 84daa5fd70ff016d21d3d274e44d74f7caae1e31 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:39:03 +0100 Subject: [PATCH 06/47] implements PLN-0090 --- .../services/vm_runtime/frame_scheduler.rs | 97 +++++++++++++++++++ .../src/services/vm_runtime/lifecycle.rs | 2 + .../src/services/vm_runtime/mod.rs | 3 + .../src/services/vm_runtime/tick.rs | 12 +++ discussion/index.ndjson | 2 +- ...N-0090-add-render-frame-pacing-contract.md | 2 +- 6 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 crates/console/prometeu-system/src/services/vm_runtime/frame_scheduler.rs diff --git a/crates/console/prometeu-system/src/services/vm_runtime/frame_scheduler.rs b/crates/console/prometeu-system/src/services/vm_runtime/frame_scheduler.rs new file mode 100644 index 00000000..00ded757 --- /dev/null +++ b/crates/console/prometeu-system/src/services/vm_runtime/frame_scheduler.rs @@ -0,0 +1,97 @@ +use prometeu_hal::FrameId; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameAuthorizationError { + GameFrameAlreadyActive { active_frame_id: FrameId }, +} + +#[derive(Debug, Clone, Default)] +pub struct FrameScheduler { + next_game_frame_id: FrameId, + active_game_frame_id: Option, + completed_game_frames: u64, + logical_frame_overruns: u64, +} + +impl FrameScheduler { + pub fn authorize_game_frame(&mut self) -> Result { + if let Some(active_frame_id) = self.active_game_frame_id { + return Err(FrameAuthorizationError::GameFrameAlreadyActive { active_frame_id }); + } + + let frame_id = self.next_game_frame_id; + self.next_game_frame_id = self.next_game_frame_id.next(); + self.active_game_frame_id = Some(frame_id); + Ok(frame_id) + } + + pub fn complete_game_frame(&mut self) -> Option { + let completed = self.active_game_frame_id.take(); + if completed.is_some() { + self.completed_game_frames = self.completed_game_frames.wrapping_add(1); + } + completed + } + + pub fn record_logical_frame_overrun(&mut self) { + self.logical_frame_overruns = self.logical_frame_overruns.wrapping_add(1); + } + + pub fn active_game_frame_id(&self) -> Option { + self.active_game_frame_id + } + + pub fn completed_game_frames(&self) -> u64 { + self.completed_game_frames + } + + pub fn logical_frame_overruns(&self) -> u64 { + self.logical_frame_overruns + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn authorizes_one_game_frame_until_completion() { + let mut scheduler = FrameScheduler::default(); + + assert_eq!(scheduler.authorize_game_frame(), Ok(FrameId::ZERO)); + assert_eq!( + scheduler.authorize_game_frame(), + Err(FrameAuthorizationError::GameFrameAlreadyActive { active_frame_id: FrameId::ZERO }) + ); + + assert_eq!(scheduler.complete_game_frame(), Some(FrameId::ZERO)); + assert_eq!(scheduler.authorize_game_frame(), Ok(FrameId::new(1))); + } + + #[test] + fn completion_is_sequential_without_catch_up_frames() { + let mut scheduler = FrameScheduler::default(); + + for expected in 0..3 { + let frame_id = scheduler.authorize_game_frame().expect("frame authorization"); + assert_eq!(frame_id, FrameId::new(expected)); + assert_eq!(scheduler.complete_game_frame(), Some(FrameId::new(expected))); + } + + assert_eq!(scheduler.completed_game_frames(), 3); + } + + #[test] + fn records_logical_frame_overrun_without_advancing_frame_id() { + let mut scheduler = FrameScheduler::default(); + + assert_eq!(scheduler.authorize_game_frame(), Ok(FrameId::ZERO)); + scheduler.record_logical_frame_overrun(); + scheduler.record_logical_frame_overrun(); + + assert_eq!(scheduler.logical_frame_overruns(), 2); + assert_eq!(scheduler.active_game_frame_id(), Some(FrameId::ZERO)); + assert_eq!(scheduler.complete_game_frame(), Some(FrameId::ZERO)); + assert_eq!(scheduler.authorize_game_frame(), Ok(FrameId::new(1))); + } +} diff --git a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs index 8fab82b8..10b570b9 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs @@ -22,6 +22,7 @@ impl VirtualMachineRuntime { current_cartridge_title: String::new(), current_cartridge_app_version: String::new(), current_cartridge_app_mode: AppMode::Game, + frame_scheduler: FrameScheduler::default(), render_manager: RenderManager::new(AppMode::Game), gfx2d_commands: Vec::new(), gfxui_commands: Vec::new(), @@ -119,6 +120,7 @@ impl VirtualMachineRuntime { self.current_cartridge_title.clear(); self.current_cartridge_app_version.clear(); self.current_cartridge_app_mode = AppMode::Game; + self.frame_scheduler = FrameScheduler::default(); self.render_manager = RenderManager::new(AppMode::Game); self.gfx2d_commands.clear(); self.gfxui_commands.clear(); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index 41aeeb6f..c6b9ec44 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -1,4 +1,5 @@ mod dispatch; +mod frame_scheduler; mod lifecycle; pub mod render_manager; #[cfg(test)] @@ -6,6 +7,7 @@ mod tests; mod tick; use crate::CrashReport; +pub use frame_scheduler::FrameScheduler; use prometeu_bytecode::string_materialization_count; use prometeu_hal::app_mode::AppMode; use prometeu_hal::log::LogService; @@ -28,6 +30,7 @@ pub struct VirtualMachineRuntime { pub current_cartridge_title: String, pub current_cartridge_app_version: String, pub current_cartridge_app_mode: AppMode, + pub frame_scheduler: FrameScheduler, pub render_manager: RenderManager, pub gfx2d_commands: Vec, pub gfxui_commands: Vec, diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index fa26df0b..e242b805 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -141,6 +141,11 @@ impl VirtualMachineRuntime { self.update_fs(log_service, fs, fs_state); if !self.logical_frame_active { + if self.current_cartridge_app_mode == AppMode::Game { + self.frame_scheduler + .authorize_game_frame() + .expect("logical frame cannot start while another Game frame is active"); + } self.logical_frame_active = true; self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME; self.begin_logical_frame(signals, hw); @@ -317,6 +322,9 @@ impl VirtualMachineRuntime { self.logical_frame_index += 1; self.logical_frame_active = false; self.logical_frame_remaining_cycles = 0; + if self.current_cartridge_app_mode == AppMode::Game { + self.frame_scheduler.complete_game_frame(); + } if run.reason == LogicalFrameEndingReason::FrameSync { self.needs_prepare_entry_call = true; @@ -326,6 +334,10 @@ impl VirtualMachineRuntime { self.paused = true; self.debug_step_request = false; } + } else if run.reason == LogicalFrameEndingReason::BudgetExhausted + && self.current_cartridge_app_mode == AppMode::Game + { + self.frame_scheduler.record_logical_frame_overrun(); } } Err(e) => { diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 8152b547..35c3813e 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md b/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md index d097ee40..78511780 100644 --- a/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md +++ b/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md @@ -2,7 +2,7 @@ id: PLN-0090 ticket: vm-render-parallel-execution-boundary title: Add Render Frame Pacing Contract -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, frame-clock, scheduler, vm] From dcfc258aeb9d3fda9ed2c97dcfa633cbff3609dc Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:41:09 +0100 Subject: [PATCH 07/47] implements PLN-0091 --- .../src/services/vm_runtime/render_manager.rs | 80 +++++++++++++++++++ .../src/services/vm_runtime/tests.rs | 3 + .../src/services/vm_runtime/tick.rs | 11 ++- discussion/index.ndjson | 2 +- ...-implement-appmode-render-policy-matrix.md | 2 +- 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs index 453b63f3..715953db 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs @@ -14,6 +14,46 @@ pub enum RenderSubmissionError { PacketAppModeMismatch { active: AppMode }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderPacingPolicy { + FrameScheduled, + LifecycleDriven, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderExecutionPolicy { + LocalSynchronous, + WorkerCapableWithLocalFallback, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RenderPolicy { + pub app_mode: AppMode, + pub pacing: RenderPacingPolicy, + pub execution: RenderExecutionPolicy, +} + +impl RenderPolicy { + pub const fn for_app_mode(app_mode: AppMode) -> Self { + match app_mode { + AppMode::Game => Self { + app_mode, + pacing: RenderPacingPolicy::FrameScheduled, + execution: RenderExecutionPolicy::WorkerCapableWithLocalFallback, + }, + AppMode::Shell => Self { + app_mode, + pacing: RenderPacingPolicy::LifecycleDriven, + execution: RenderExecutionPolicy::LocalSynchronous, + }, + } + } + + pub const fn uses_frame_scheduler(self) -> bool { + matches!(self.pacing, RenderPacingPolicy::FrameScheduled) + } +} + pub trait RenderSurface { fn consume_submission(&mut self, submission: &RenderSubmission); } @@ -69,6 +109,7 @@ impl RenderHandoff { #[derive(Debug, Clone)] pub struct RenderManager { active_app_mode: AppMode, + active_policy: RenderPolicy, next_frame_id: FrameId, handoff: RenderHandoff, transition_state: RenderTransitionState, @@ -78,6 +119,7 @@ impl RenderManager { pub fn new(active_app_mode: AppMode) -> Self { Self { active_app_mode, + active_policy: RenderPolicy::for_app_mode(active_app_mode), next_frame_id: FrameId::ZERO, handoff: RenderHandoff::default(), transition_state: RenderTransitionState::Idle, @@ -88,6 +130,14 @@ impl RenderManager { self.active_app_mode } + pub fn active_render_policy(&self) -> RenderPolicy { + self.active_policy + } + + pub fn policy_for_app_mode(app_mode: AppMode) -> RenderPolicy { + RenderPolicy::for_app_mode(app_mode) + } + pub fn transition_state(&self) -> RenderTransitionState { self.transition_state } @@ -119,6 +169,7 @@ impl RenderManager { let previous = self.active_app_mode; self.active_app_mode = app_mode; + self.active_policy = RenderPolicy::for_app_mode(app_mode); self.transition_state = RenderTransitionState::Pending { from: previous, to: app_mode }; } @@ -239,6 +290,35 @@ mod tests { assert_eq!(manager.transition_state(), RenderTransitionState::Idle); } + #[test] + fn render_policy_maps_game_to_frame_scheduled_worker_capable() { + let policy = RenderPolicy::for_app_mode(AppMode::Game); + + assert_eq!(policy.pacing, RenderPacingPolicy::FrameScheduled); + assert_eq!(policy.execution, RenderExecutionPolicy::WorkerCapableWithLocalFallback); + assert!(policy.uses_frame_scheduler()); + } + + #[test] + fn render_policy_maps_shell_to_lifecycle_driven_local_sync() { + let policy = RenderPolicy::for_app_mode(AppMode::Shell); + + assert_eq!(policy.pacing, RenderPacingPolicy::LifecycleDriven); + assert_eq!(policy.execution, RenderExecutionPolicy::LocalSynchronous); + assert!(!policy.uses_frame_scheduler()); + } + + #[test] + fn active_render_policy_follows_app_mode_transition() { + let mut manager = RenderManager::new(AppMode::Game); + + assert_eq!(manager.active_render_policy(), RenderPolicy::for_app_mode(AppMode::Game)); + + manager.set_active_app_mode(AppMode::Shell); + + assert_eq!(manager.active_render_policy(), RenderPolicy::for_app_mode(AppMode::Shell)); + } + #[test] fn publish_latest_hands_submission_to_surface() { let mut manager = RenderManager::new(AppMode::Game); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs index b931c981..66ed5bb4 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs @@ -484,6 +484,8 @@ fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() { assert_eq!(packet.commands.len(), 1); assert!(matches!(packet.commands[0], prometeu_hal::GfxUiCommand::Clear { .. })); assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); + assert_eq!(runtime.frame_scheduler.completed_game_frames(), 0); + assert_eq!(runtime.frame_scheduler.active_game_frame_id(), None); } #[test] @@ -833,6 +835,7 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() { prometeu_hal::Gfx2dCommand::Clear { color } if color == Color::from_raw(0x11223344) )); assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); + assert_eq!(runtime.frame_scheduler.completed_game_frames(), 1); let second_packet = prometeu_hal::Game2DFramePacket::new( Default::default(), diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index e242b805..7046fa4b 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -141,7 +141,9 @@ impl VirtualMachineRuntime { self.update_fs(log_service, fs, fs_state); if !self.logical_frame_active { - if self.current_cartridge_app_mode == AppMode::Game { + if RenderManager::policy_for_app_mode(self.current_cartridge_app_mode) + .uses_frame_scheduler() + { self.frame_scheduler .authorize_game_frame() .expect("logical frame cannot start while another Game frame is active"); @@ -322,7 +324,9 @@ impl VirtualMachineRuntime { self.logical_frame_index += 1; self.logical_frame_active = false; self.logical_frame_remaining_cycles = 0; - if self.current_cartridge_app_mode == AppMode::Game { + if RenderManager::policy_for_app_mode(self.current_cartridge_app_mode) + .uses_frame_scheduler() + { self.frame_scheduler.complete_game_frame(); } @@ -335,7 +339,8 @@ impl VirtualMachineRuntime { self.debug_step_request = false; } } else if run.reason == LogicalFrameEndingReason::BudgetExhausted - && self.current_cartridge_app_mode == AppMode::Game + && RenderManager::policy_for_app_mode(self.current_cartridge_app_mode) + .uses_frame_scheduler() { self.frame_scheduler.record_logical_frame_overrun(); } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 35c3813e..0d6a4ff7 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md b/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md index 94be136b..9585762f 100644 --- a/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md +++ b/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md @@ -2,7 +2,7 @@ id: PLN-0091 ticket: vm-render-parallel-execution-boundary title: Implement AppMode Render Policy Matrix -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, appmode, shell, game, policy] From 3521eec319481f56b735ac5f42ae12607c2d005d Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:44:29 +0100 Subject: [PATCH 08/47] implements PLN-0092 --- crates/console/prometeu-hal/src/lib.rs | 4 +- .../prometeu-hal/src/render_submission.rs | 42 ++++++++- .../src/services/vm_runtime/lifecycle.rs | 2 +- .../src/services/vm_runtime/render_manager.rs | 94 +++++++++++++++++-- .../src/services/vm_runtime/tick.rs | 5 +- discussion/index.ndjson | 2 +- ...-add-render-epoch-ownership-transitions.md | 2 +- 7 files changed, 135 insertions(+), 16 deletions(-) diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index a9e65eef..fdd14e47 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -48,7 +48,7 @@ pub use native_interface::{NativeInterface, SyscallId}; pub use pad_bridge::PadBridge; pub use render_submission::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket, - Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderSubmission, RenderSubmissionPacket, - ShellUiFramePacket, + Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderOwnership, RenderSubmission, + RenderSubmissionPacket, ShellUiFramePacket, }; pub use touch_bridge::TouchBridge; diff --git a/crates/console/prometeu-hal/src/render_submission.rs b/crates/console/prometeu-hal/src/render_submission.rs index 3427f581..6f04e7ef 100644 --- a/crates/console/prometeu-hal/src/render_submission.rs +++ b/crates/console/prometeu-hal/src/render_submission.rs @@ -25,16 +25,32 @@ impl FrameId { pub struct RenderSubmission { pub frame_id: FrameId, pub app_mode: AppMode, + pub ownership: RenderOwnership, pub packet: RenderSubmissionPacket, } impl RenderSubmission { pub const fn game2d(frame_id: FrameId, packet: Game2DFramePacket) -> Self { - Self { frame_id, app_mode: AppMode::Game, packet: RenderSubmissionPacket::Game2D(packet) } + Self { + frame_id, + app_mode: AppMode::Game, + ownership: RenderOwnership::new(0, AppMode::Game, 0), + packet: RenderSubmissionPacket::Game2D(packet), + } } pub const fn shell_ui(frame_id: FrameId, packet: ShellUiFramePacket) -> Self { - Self { frame_id, app_mode: AppMode::Shell, packet: RenderSubmissionPacket::ShellUi(packet) } + Self { + frame_id, + app_mode: AppMode::Shell, + ownership: RenderOwnership::new(0, AppMode::Shell, 0), + packet: RenderSubmissionPacket::ShellUi(packet), + } + } + + pub const fn with_ownership(mut self, ownership: RenderOwnership) -> Self { + self.ownership = ownership; + self } pub const fn is_coherent(&self) -> bool { @@ -46,6 +62,19 @@ impl RenderSubmission { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RenderOwnership { + pub epoch: u64, + pub app_mode: AppMode, + pub app_id: u32, +} + +impl RenderOwnership { + pub const fn new(epoch: u64, app_mode: AppMode, app_id: u32) -> Self { + Self { epoch, app_mode, app_id } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum RenderSubmissionPacket { Game2D(Game2DFramePacket), @@ -164,12 +193,21 @@ mod tests { let submission = RenderSubmission { frame_id: FrameId::new(1), app_mode: AppMode::Game, + ownership: RenderOwnership::new(0, AppMode::Game, 0), packet: RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(Vec::new())), }; assert!(!submission.is_coherent()); } + #[test] + fn ownership_can_be_stamped_by_runtime() { + let submission = RenderSubmission::game2d(FrameId::new(3), Game2DFramePacket::default()) + .with_ownership(RenderOwnership::new(9, AppMode::Game, 42)); + + assert_eq!(submission.ownership, RenderOwnership::new(9, AppMode::Game, 42)); + } + #[test] fn closed_submission_owns_command_payload() { let mut commands = vec![Gfx2dCommand::DrawText { diff --git a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs index 10b570b9..7d64c02f 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs @@ -156,7 +156,7 @@ impl VirtualMachineRuntime { self.current_cartridge_title = cartridge.title.clone(); self.current_cartridge_app_version = cartridge.app_version.clone(); self.current_cartridge_app_mode = cartridge.app_mode; - self.render_manager.set_active_app_mode(cartridge.app_mode); + self.render_manager.transition_render_owner(cartridge.app_mode, cartridge.app_id); Ok(()) } Err(e) => { diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs index 715953db..6085b99a 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs @@ -1,12 +1,13 @@ use prometeu_hal::app_mode::AppMode; use prometeu_hal::{ - FrameId, Game2DFramePacket, RenderSubmission, RenderSubmissionPacket, ShellUiFramePacket, + FrameId, Game2DFramePacket, RenderOwnership, RenderSubmission, RenderSubmissionPacket, + ShellUiFramePacket, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RenderTransitionState { Idle, - Pending { from: AppMode, to: AppMode }, + Pending { from: RenderOwnership, to: RenderOwnership }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -64,6 +65,7 @@ pub struct RenderHandoff { last_consumed: Option, replaced_before_consume: u64, discarded_pending: u64, + stale_epoch_discards: u64, } impl RenderHandoff { @@ -89,6 +91,11 @@ impl RenderHandoff { self.last_consumed = Some(submission); } + pub fn record_stale_epoch_discard(&mut self, submission: RenderSubmission) { + self.stale_epoch_discards = self.stale_epoch_discards.wrapping_add(1); + self.last_consumed = Some(submission); + } + pub fn discard_pending(&mut self) -> Option { let discarded = self.pending.take(); if discarded.is_some() { @@ -104,11 +111,17 @@ impl RenderHandoff { pub fn discarded_pending(&self) -> u64 { self.discarded_pending } + + pub fn stale_epoch_discards(&self) -> u64 { + self.stale_epoch_discards + } } #[derive(Debug, Clone)] pub struct RenderManager { active_app_mode: AppMode, + active_app_id: u32, + active_ownership: RenderOwnership, active_policy: RenderPolicy, next_frame_id: FrameId, handoff: RenderHandoff, @@ -119,6 +132,8 @@ impl RenderManager { pub fn new(active_app_mode: AppMode) -> Self { Self { active_app_mode, + active_app_id: 0, + active_ownership: RenderOwnership::new(0, active_app_mode, 0), active_policy: RenderPolicy::for_app_mode(active_app_mode), next_frame_id: FrameId::ZERO, handoff: RenderHandoff::default(), @@ -130,6 +145,10 @@ impl RenderManager { self.active_app_mode } + pub fn active_ownership(&self) -> RenderOwnership { + self.active_ownership + } + pub fn active_render_policy(&self) -> RenderPolicy { self.active_policy } @@ -158,19 +177,31 @@ impl RenderManager { self.handoff.discarded_pending() } + pub fn stale_epoch_discards(&self) -> u64 { + self.handoff.stale_epoch_discards() + } + pub fn discard_pending_submission(&mut self) -> Option { self.handoff.discard_pending() } pub fn set_active_app_mode(&mut self, app_mode: AppMode) { - if self.active_app_mode == app_mode { + self.transition_render_owner(app_mode, self.active_app_id); + } + + pub fn transition_render_owner(&mut self, app_mode: AppMode, app_id: u32) { + if self.active_app_mode == app_mode && self.active_app_id == app_id { return; } - let previous = self.active_app_mode; + let previous = self.active_ownership; self.active_app_mode = app_mode; + self.active_app_id = app_id; + self.active_ownership = + RenderOwnership::new(self.active_ownership.epoch.wrapping_add(1), app_mode, app_id); self.active_policy = RenderPolicy::for_app_mode(app_mode); - self.transition_state = RenderTransitionState::Pending { from: previous, to: app_mode }; + self.transition_state = + RenderTransitionState::Pending { from: previous, to: self.active_ownership }; } pub fn acknowledge_transition(&mut self) { @@ -184,10 +215,10 @@ impl RenderManager { let frame_id = self.next_frame_id; let submission = match (self.active_app_mode, packet) { (AppMode::Game, RenderSubmissionPacket::Game2D(packet)) => { - RenderSubmission::game2d(frame_id, packet) + RenderSubmission::game2d(frame_id, packet).with_ownership(self.active_ownership) } (AppMode::Shell, RenderSubmissionPacket::ShellUi(packet)) => { - RenderSubmission::shell_ui(frame_id, packet) + RenderSubmission::shell_ui(frame_id, packet).with_ownership(self.active_ownership) } (active, _) => { return Err(RenderSubmissionError::PacketAppModeMismatch { active }); @@ -212,6 +243,10 @@ impl RenderManager { let Some(submission) = self.handoff.take_latest() else { return false; }; + if submission.ownership != self.active_ownership { + self.handoff.record_stale_epoch_discard(submission); + return false; + } surface.consume_submission(&submission); self.handoff.record_consumed(submission); true @@ -277,19 +312,45 @@ mod tests { #[test] fn app_mode_switch_records_noop_transition_placeholder() { let mut manager = RenderManager::new(AppMode::Game); + let previous = manager.active_ownership(); manager.set_active_app_mode(AppMode::Shell); assert_eq!(manager.active_app_mode(), AppMode::Shell); assert_eq!( manager.transition_state(), - RenderTransitionState::Pending { from: AppMode::Game, to: AppMode::Shell } + RenderTransitionState::Pending { + from: previous, + to: RenderOwnership::new(1, AppMode::Shell, 0) + } ); manager.acknowledge_transition(); assert_eq!(manager.transition_state(), RenderTransitionState::Idle); } + #[test] + fn transition_render_owner_increments_epoch_for_same_mode_app_swap() { + let mut manager = RenderManager::new(AppMode::Game); + + manager.transition_render_owner(AppMode::Game, 7); + let first_owner = manager.active_ownership(); + manager.transition_render_owner(AppMode::Game, 8); + + assert_eq!(first_owner, RenderOwnership::new(1, AppMode::Game, 7)); + assert_eq!(manager.active_ownership(), RenderOwnership::new(2, AppMode::Game, 8)); + } + + #[test] + fn close_frame_stamps_submission_with_active_ownership() { + let mut manager = RenderManager::new(AppMode::Game); + manager.transition_render_owner(AppMode::Game, 42); + + let submission = manager.close_compat_frame(); + + assert_eq!(submission.ownership, RenderOwnership::new(1, AppMode::Game, 42)); + } + #[test] fn render_policy_maps_game_to_frame_scheduled_worker_capable() { let policy = RenderPolicy::for_app_mode(AppMode::Game); @@ -382,4 +443,21 @@ mod tests { FrameId::ZERO ); } + + #[test] + fn publish_latest_discards_stale_epoch_before_present() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + + manager.close_compat_frame(); + manager.transition_render_owner(AppMode::Shell, 1); + + assert!(!manager.publish_latest(&mut surface)); + assert!(surface.seen.is_empty()); + assert_eq!(manager.stale_epoch_discards(), 1); + assert_eq!( + manager.latest_complete_submission().expect("stale consumed for diagnostics").ownership, + RenderOwnership::new(0, AppMode::Game, 0) + ); + } } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index 7046fa4b..40d3782c 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -241,7 +241,10 @@ impl VirtualMachineRuntime { if run.reason == LogicalFrameEndingReason::FrameSync || run.reason == LogicalFrameEndingReason::EndOfRom { - self.render_manager.set_active_app_mode(self.current_cartridge_app_mode); + self.render_manager.transition_render_owner( + self.current_cartridge_app_mode, + self.current_app_id, + ); if self.current_cartridge_app_mode == AppMode::Game { let mut packet = hw.close_game2d_packet(); packet.gfx2d = std::mem::take(&mut self.gfx2d_commands); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 0d6a4ff7..474f6f9e 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md b/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md index 4746f5fc..57f05e98 100644 --- a/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md +++ b/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md @@ -2,7 +2,7 @@ id: PLN-0092 ticket: vm-render-parallel-execution-boundary title: Add Render Epoch Ownership Transitions -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, lifecycle, render-epoch, ownership] From c88c927fee007ebe46a4f83dffd46e9b7d57347d Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:47:49 +0100 Subject: [PATCH 09/47] implements PLN-0093 --- crates/console/prometeu-hal/src/telemetry.rs | 64 ++++++ .../src/services/vm_runtime/render_manager.rs | 192 +++++++++++++++++- .../src/services/vm_runtime/tick.rs | 1 + discussion/index.ndjson | 2 +- ...dd-render-telemetry-counters-and-events.md | 2 +- 5 files changed, 258 insertions(+), 3 deletions(-) diff --git a/crates/console/prometeu-hal/src/telemetry.rs b/crates/console/prometeu-hal/src/telemetry.rs index 21a196ab..17253ac7 100644 --- a/crates/console/prometeu-hal/src/telemetry.rs +++ b/crates/console/prometeu-hal/src/telemetry.rs @@ -29,6 +29,23 @@ pub struct TelemetryFrame { // Log Pressure from the last completed logical frame pub logs_count: u32, + + // Render handoff/presentation telemetry + pub produced_submissions: u64, + pub replaced_before_consume: u64, + pub consumed_submissions: u64, + pub presented_frames: u64, + pub repeated_presents: u64, + pub render_errors: u64, + pub present_errors: u64, + pub stale_epoch_discards: u64, + pub shutdown_discards: u64, + pub last_produced_frame_id: u64, + pub last_consumed_frame_id: u64, + pub last_presented_frame_id: u64, + pub last_dropped_frame_id: u64, + pub last_error_frame_id: u64, + pub active_render_epoch: u64, } /// Thread-safe, atomic telemetry storage for real-time monitoring by the host. @@ -62,6 +79,23 @@ pub struct AtomicTelemetry { pub current_logs_count: Arc, // Persisted log count from the last completed logical frame pub logs_count: AtomicU32, + + // Render handoff/presentation telemetry + pub produced_submissions: AtomicU64, + pub replaced_before_consume: AtomicU64, + pub consumed_submissions: AtomicU64, + pub presented_frames: AtomicU64, + pub repeated_presents: AtomicU64, + pub render_errors: AtomicU64, + pub present_errors: AtomicU64, + pub stale_epoch_discards: AtomicU64, + pub shutdown_discards: AtomicU64, + pub last_produced_frame_id: AtomicU64, + pub last_consumed_frame_id: AtomicU64, + pub last_presented_frame_id: AtomicU64, + pub last_dropped_frame_id: AtomicU64, + pub last_error_frame_id: AtomicU64, + pub active_render_epoch: AtomicU64, } impl AtomicTelemetry { @@ -91,6 +125,21 @@ impl AtomicTelemetry { vm_string_materializations: self.vm_string_materializations.load(Ordering::Relaxed), logs_count: self.logs_count.load(Ordering::Relaxed), vm_steps: self.vm_steps.load(Ordering::Relaxed), + produced_submissions: self.produced_submissions.load(Ordering::Relaxed), + replaced_before_consume: self.replaced_before_consume.load(Ordering::Relaxed), + consumed_submissions: self.consumed_submissions.load(Ordering::Relaxed), + presented_frames: self.presented_frames.load(Ordering::Relaxed), + repeated_presents: self.repeated_presents.load(Ordering::Relaxed), + render_errors: self.render_errors.load(Ordering::Relaxed), + present_errors: self.present_errors.load(Ordering::Relaxed), + stale_epoch_discards: self.stale_epoch_discards.load(Ordering::Relaxed), + shutdown_discards: self.shutdown_discards.load(Ordering::Relaxed), + last_produced_frame_id: self.last_produced_frame_id.load(Ordering::Relaxed), + last_consumed_frame_id: self.last_consumed_frame_id.load(Ordering::Relaxed), + last_presented_frame_id: self.last_presented_frame_id.load(Ordering::Relaxed), + last_dropped_frame_id: self.last_dropped_frame_id.load(Ordering::Relaxed), + last_error_frame_id: self.last_error_frame_id.load(Ordering::Relaxed), + active_render_epoch: self.active_render_epoch.load(Ordering::Relaxed), } } @@ -113,6 +162,21 @@ impl AtomicTelemetry { self.vm_steps.store(0, Ordering::Relaxed); self.logs_count.store(0, Ordering::Relaxed); self.current_logs_count.store(0, Ordering::Relaxed); + self.produced_submissions.store(0, Ordering::Relaxed); + self.replaced_before_consume.store(0, Ordering::Relaxed); + self.consumed_submissions.store(0, Ordering::Relaxed); + self.presented_frames.store(0, Ordering::Relaxed); + self.repeated_presents.store(0, Ordering::Relaxed); + self.render_errors.store(0, Ordering::Relaxed); + self.present_errors.store(0, Ordering::Relaxed); + self.stale_epoch_discards.store(0, Ordering::Relaxed); + self.shutdown_discards.store(0, Ordering::Relaxed); + self.last_produced_frame_id.store(0, Ordering::Relaxed); + self.last_consumed_frame_id.store(0, Ordering::Relaxed); + self.last_presented_frame_id.store(0, Ordering::Relaxed); + self.last_dropped_frame_id.store(0, Ordering::Relaxed); + self.last_error_frame_id.store(0, Ordering::Relaxed); + self.active_render_epoch.store(0, Ordering::Relaxed); } } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs index 6085b99a..6241e955 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs @@ -1,8 +1,10 @@ use prometeu_hal::app_mode::AppMode; +use prometeu_hal::telemetry::AtomicTelemetry; use prometeu_hal::{ FrameId, Game2DFramePacket, RenderOwnership, RenderSubmission, RenderSubmissionPacket, ShellUiFramePacket, }; +use std::sync::atomic::Ordering; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RenderTransitionState { @@ -59,6 +61,78 @@ pub trait RenderSurface { fn consume_submission(&mut self, submission: &RenderSubmission); } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct RenderTelemetrySnapshot { + pub produced_submissions: u64, + pub replaced_before_consume: u64, + pub consumed_submissions: u64, + pub presented_frames: u64, + pub repeated_presents: u64, + pub render_errors: u64, + pub present_errors: u64, + pub stale_epoch_discards: u64, + pub shutdown_discards: u64, + pub last_produced_frame_id: u64, + pub last_consumed_frame_id: u64, + pub last_presented_frame_id: u64, + pub last_dropped_frame_id: u64, + pub last_error_frame_id: u64, + pub active_render_epoch: u64, +} + +#[derive(Debug, Clone, Default)] +struct RenderTelemetry { + snapshot: RenderTelemetrySnapshot, +} + +impl RenderTelemetry { + fn record_produced(&mut self, frame_id: FrameId) { + self.snapshot.produced_submissions = self.snapshot.produced_submissions.wrapping_add(1); + self.snapshot.last_produced_frame_id = frame_id.get(); + } + + fn record_replaced_before_consume(&mut self, frame_id: FrameId) { + self.snapshot.replaced_before_consume = + self.snapshot.replaced_before_consume.wrapping_add(1); + self.snapshot.last_dropped_frame_id = frame_id.get(); + } + + fn record_consumed(&mut self, frame_id: FrameId) { + self.snapshot.consumed_submissions = self.snapshot.consumed_submissions.wrapping_add(1); + self.snapshot.last_consumed_frame_id = frame_id.get(); + } + + fn record_presented(&mut self, frame_id: FrameId) { + self.snapshot.presented_frames = self.snapshot.presented_frames.wrapping_add(1); + self.snapshot.last_presented_frame_id = frame_id.get(); + } + + fn record_repeated_present(&mut self, frame_id: FrameId) { + self.snapshot.repeated_presents = self.snapshot.repeated_presents.wrapping_add(1); + self.snapshot.last_presented_frame_id = frame_id.get(); + } + + fn record_render_error(&mut self, frame_id: FrameId) { + self.snapshot.render_errors = self.snapshot.render_errors.wrapping_add(1); + self.snapshot.last_error_frame_id = frame_id.get(); + } + + fn record_present_error(&mut self, frame_id: FrameId) { + self.snapshot.present_errors = self.snapshot.present_errors.wrapping_add(1); + self.snapshot.last_error_frame_id = frame_id.get(); + } + + fn record_stale_epoch_discard(&mut self, frame_id: FrameId) { + self.snapshot.stale_epoch_discards = self.snapshot.stale_epoch_discards.wrapping_add(1); + self.snapshot.last_dropped_frame_id = frame_id.get(); + } + + fn record_shutdown_discard(&mut self, frame_id: FrameId) { + self.snapshot.shutdown_discards = self.snapshot.shutdown_discards.wrapping_add(1); + self.snapshot.last_dropped_frame_id = frame_id.get(); + } +} + #[derive(Debug, Clone, Default)] pub struct RenderHandoff { pending: Option, @@ -125,6 +199,7 @@ pub struct RenderManager { active_policy: RenderPolicy, next_frame_id: FrameId, handoff: RenderHandoff, + telemetry: RenderTelemetry, transition_state: RenderTransitionState, } @@ -137,6 +212,7 @@ impl RenderManager { active_policy: RenderPolicy::for_app_mode(active_app_mode), next_frame_id: FrameId::ZERO, handoff: RenderHandoff::default(), + telemetry: RenderTelemetry::default(), transition_state: RenderTransitionState::Idle, } } @@ -182,7 +258,52 @@ impl RenderManager { } pub fn discard_pending_submission(&mut self) -> Option { - self.handoff.discard_pending() + let discarded = self.handoff.discard_pending(); + if let Some(submission) = discarded.as_ref() { + self.telemetry.record_shutdown_discard(submission.frame_id); + } + discarded + } + + pub fn render_telemetry(&self) -> RenderTelemetrySnapshot { + let mut snapshot = self.telemetry.snapshot; + snapshot.active_render_epoch = self.active_ownership.epoch; + snapshot + } + + pub fn sync_telemetry(&self, telemetry: &AtomicTelemetry) { + let snapshot = self.render_telemetry(); + telemetry.produced_submissions.store(snapshot.produced_submissions, Ordering::Relaxed); + telemetry + .replaced_before_consume + .store(snapshot.replaced_before_consume, Ordering::Relaxed); + telemetry.consumed_submissions.store(snapshot.consumed_submissions, Ordering::Relaxed); + telemetry.presented_frames.store(snapshot.presented_frames, Ordering::Relaxed); + telemetry.repeated_presents.store(snapshot.repeated_presents, Ordering::Relaxed); + telemetry.render_errors.store(snapshot.render_errors, Ordering::Relaxed); + telemetry.present_errors.store(snapshot.present_errors, Ordering::Relaxed); + telemetry.stale_epoch_discards.store(snapshot.stale_epoch_discards, Ordering::Relaxed); + telemetry.shutdown_discards.store(snapshot.shutdown_discards, Ordering::Relaxed); + telemetry.last_produced_frame_id.store(snapshot.last_produced_frame_id, Ordering::Relaxed); + telemetry.last_consumed_frame_id.store(snapshot.last_consumed_frame_id, Ordering::Relaxed); + telemetry + .last_presented_frame_id + .store(snapshot.last_presented_frame_id, Ordering::Relaxed); + telemetry.last_dropped_frame_id.store(snapshot.last_dropped_frame_id, Ordering::Relaxed); + telemetry.last_error_frame_id.store(snapshot.last_error_frame_id, Ordering::Relaxed); + telemetry.active_render_epoch.store(snapshot.active_render_epoch, Ordering::Relaxed); + } + + pub fn record_repeated_present(&mut self, frame_id: FrameId) { + self.telemetry.record_repeated_present(frame_id); + } + + pub fn record_render_error(&mut self, frame_id: FrameId) { + self.telemetry.record_render_error(frame_id); + } + + pub fn record_present_error(&mut self, frame_id: FrameId) { + self.telemetry.record_present_error(frame_id); } pub fn set_active_app_mode(&mut self, app_mode: AppMode) { @@ -226,6 +347,10 @@ impl RenderManager { }; self.next_frame_id = self.next_frame_id.next(); + if let Some(replaced) = self.handoff.pending_submission() { + self.telemetry.record_replaced_before_consume(replaced.frame_id); + } + self.telemetry.record_produced(frame_id); self.handoff.publish(submission); Ok(self.handoff.pending_submission().expect("submission was just stored")) } @@ -244,10 +369,13 @@ impl RenderManager { return false; }; if submission.ownership != self.active_ownership { + self.telemetry.record_stale_epoch_discard(submission.frame_id); self.handoff.record_stale_epoch_discard(submission); return false; } + self.telemetry.record_consumed(submission.frame_id); surface.consume_submission(&submission); + self.telemetry.record_presented(submission.frame_id); self.handoff.record_consumed(submission); true } @@ -262,6 +390,8 @@ impl Default for RenderManager { #[cfg(test)] mod tests { use super::*; + use std::sync::Arc; + use std::sync::atomic::AtomicU32; #[derive(Default)] struct RecordingSurface { @@ -460,4 +590,64 @@ mod tests { RenderOwnership::new(0, AppMode::Game, 0) ); } + + #[test] + fn render_telemetry_counts_produced_replaced_consumed_and_presented() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + + manager.close_compat_frame(); + manager.close_compat_frame(); + assert!(manager.publish_latest(&mut surface)); + + let telemetry = manager.render_telemetry(); + assert_eq!(telemetry.produced_submissions, 2); + assert_eq!(telemetry.replaced_before_consume, 1); + assert_eq!(telemetry.consumed_submissions, 1); + assert_eq!(telemetry.presented_frames, 1); + assert_eq!(telemetry.last_produced_frame_id, 1); + assert_eq!(telemetry.last_dropped_frame_id, 0); + assert_eq!(telemetry.last_consumed_frame_id, 1); + assert_eq!(telemetry.last_presented_frame_id, 1); + } + + #[test] + fn render_telemetry_counts_stale_repeat_and_error_hooks() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + + manager.close_compat_frame(); + manager.transition_render_owner(AppMode::Shell, 1); + assert!(!manager.publish_latest(&mut surface)); + manager.record_repeated_present(FrameId::new(9)); + manager.record_render_error(FrameId::new(10)); + manager.record_present_error(FrameId::new(11)); + + let telemetry = manager.render_telemetry(); + assert_eq!(telemetry.stale_epoch_discards, 1); + assert_eq!(telemetry.repeated_presents, 1); + assert_eq!(telemetry.render_errors, 1); + assert_eq!(telemetry.present_errors, 1); + assert_eq!(telemetry.last_presented_frame_id, 9); + assert_eq!(telemetry.last_error_frame_id, 11); + } + + #[test] + fn render_telemetry_syncs_to_atomic_telemetry() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + let atomic = AtomicTelemetry::new(Arc::new(AtomicU32::new(0))); + + manager.transition_render_owner(AppMode::Game, 5); + manager.close_compat_frame(); + assert!(manager.publish_latest(&mut surface)); + manager.sync_telemetry(&atomic); + + let snapshot = atomic.snapshot(); + assert_eq!(snapshot.produced_submissions, 1); + assert_eq!(snapshot.consumed_submissions, 1); + assert_eq!(snapshot.presented_frames, 1); + assert_eq!(snapshot.last_presented_frame_id, 0); + assert_eq!(snapshot.active_render_epoch, 1); + } } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index 40d3782c..f4aae550 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -304,6 +304,7 @@ impl VirtualMachineRuntime { self.atomic_telemetry .host_cpu_time_us .store(start.elapsed().as_micros() as u64, Ordering::Relaxed); + self.render_manager.sync_telemetry(&self.atomic_telemetry); let current_frame_logs = self.atomic_telemetry.current_logs_count.load(Ordering::Relaxed); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 474f6f9e..88b7b37a 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md b/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md index 54dba281..2fd67371 100644 --- a/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md +++ b/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md @@ -2,7 +2,7 @@ id: PLN-0093 ticket: vm-render-parallel-execution-boundary title: Add Render Telemetry Counters and Events -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, renderer, telemetry, diagnostics] From f01822bd533752d2139055fa69f8fe73cc8cd328 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:49:59 +0100 Subject: [PATCH 10/47] implements PLN-0094 --- discussion/index.ndjson | 2 +- .../PLN-0094-specify-async-render-boundary.md | 2 +- docs/specs/runtime/04-gfx-peripheral.md | 190 +++++++++++++++--- 3 files changed, 162 insertions(+), 32 deletions(-) diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 88b7b37a..475939ef 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md b/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md index 71411357..7b0ac40d 100644 --- a/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md +++ b/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md @@ -2,7 +2,7 @@ id: PLN-0094 ticket: vm-render-parallel-execution-boundary title: Specify Async Render Boundary -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [spec, runtime, renderer, architecture] diff --git a/docs/specs/runtime/04-gfx-peripheral.md b/docs/specs/runtime/04-gfx-peripheral.md index ea591aaf..6f545436 100644 --- a/docs/specs/runtime/04-gfx-peripheral.md +++ b/docs/specs/runtime/04-gfx-peripheral.md @@ -92,7 +92,137 @@ This guarantees: --- -## 4. PROMETEU Graphical Structure +## 4. Asynchronous Render Boundary + +`DEC-0031` defines the asynchronous render boundary. The contract applies even +when the current implementation consumes submissions synchronously. + +### 4.1 Complete packet contract + +`RenderSubmissionPacket::Game2D` MUST be a complete frame description for the +Game 2D render consumer. The consumer MUST NOT bypass the packet by consulting +live VM state, live `FrameComposer` state, or mutable runtime state. + +For Game 2D, the required composition order is: + +```text +composer scene/layers/sprites/HUD +-> buffered gfx2d primitives +-> publication/present +``` + +`gfx2d.*` primitives are therefore a final Game 2D overlay. This is true for +both active-scene frames and no-scene frames. + +### 4.2 Resource boundary + +Submissions MUST remain small and owned. Heavy resident resources such as glyph +banks, scene banks, asset payloads, and viewport cache materializations MUST +NOT be copied into each submission. + +Submissions SHALL carry stable resource IDs or handles. The render consumer may +resolve those IDs through read-only resource APIs. Resource installation, +resolver updates, viewport cache refreshes, and bank residency changes belong +to the logical/runtime side before handoff, or to an owning service. They MUST +NOT require the render consumer to hold mutable VM, `Hardware`, `Gfx`, or +`FrameComposer` references. + +The runtime does not guarantee visual integrity if a developer or framework +replaces resources behind an in-flight submission in a way that violates asset +discipline. + +### 4.3 Handoff + +The first asynchronous Game render model uses a single pending slot with +latest-wins semantics: + +- publishing a new submission replaces the previous pending submission; +- replacement before consumption is counted as a render drop; +- the render consumer takes ownership of the pending submission; +- the VM/producer MUST NOT block on render consumption, raster completion, or + present completion. + +The consumer status is telemetry. It is not a semantic ACK to the VM. + +### 4.4 Frame pacing + +Game logical frames are paced by the runtime frame scheduler, not by render +worker ACK. The intended cadence is one Game logical frame per frame tick and +at most one pending submission ahead of the consumer. + +`FRAME_SYNC` remains the canonical end of a VM logical frame. Cycle/time budget +is evidence for certification, watchdog, diagnostics, and overrun reporting; it +MUST NOT be used as the normal mechanism for cutting a logical frame short. + +If a Game logical frame overruns the display cadence, logical frames remain +sequential. The render consumer may repeat the last valid frame, and telemetry +records the overrun/repeat. The VM MUST NOT produce catch-up frames to skip +from frame `N` to frame `N+k`. + +### 4.5 AppMode policy + +Render execution policy is explicit by pipeline/AppMode: + +- `AppMode::Game` is frame-paced and may use a render worker when the + host/runtime supports it, with local synchronous fallback. +- `AppMode::Shell` is lifecycle-driven and local/synchronous by default. +- Shell VM-backed apps follow Shell lifecycle; they do not declare an + independent frame-paced game workload under this contract. +- Splash, crash, and hub/system screens follow Shell/local policy unless a + later decision defines a more specific policy. + +### 4.6 Ownership and epoch + +Every submission that may cross an asynchronous boundary MUST carry render +ownership metadata: at minimum frame identity, app mode, app identity where +available, and render epoch/generation. + +Foreground visual-owner transitions MUST advance the active render epoch or +equivalent generation through a central runtime render manager. The render +consumer MUST check ownership before present and MUST discard stale submissions +whose ownership no longer matches the active owner. + +Transitions that invalidate stale render work include: + +- Game to Shell/Hub; +- Shell/Hub to Game; +- crash screen takeover; +- splash or system screen takeover; +- cartridge or app swap, even if `AppMode` remains unchanged; +- shutdown/stop. + +The same physical surface may be reused by multiple visual owners, but logical +ownership MUST remain explicit. + +Game pause/resume, foreground stack behavior, and coexistence of a paused Game +with VM-backed Shell apps are outside this contract and are tracked by the +foreground/lifecycle discussion. + +### 4.7 Render telemetry + +Asynchronous render is best-effort observable, not a VM-visible handshake. +Render drops, stale epoch discards, repeated presents, render errors, and +present errors MUST be recorded for host diagnostics, debugging, profiling, and +certification evidence. VM program semantics MUST NOT depend on whether a +submission was consumed or presented. + +Minimum render telemetry includes: + +- produced submissions; +- replaced-before-consume submissions; +- consumed submissions; +- presented frames; +- repeated presents; +- render errors; +- present errors; +- stale epoch discards; +- shutdown discards; +- last produced, consumed, presented, dropped, and error frame IDs; +- active render epoch. + +--- + +## 5. PROMETEU Graphical Structure The graphical world is composed of: @@ -101,7 +231,7 @@ The graphical world is composed of: - **1 HUD Layer** (fixed, always on top) - Sprites with priority between layers -### 4.1 Tile Banks +### 5.1 Tile Banks - There are up to **16 banks** - Each bank has a fixed tile size: @@ -115,7 +245,7 @@ The graphical world is composed of: - serialized pixels are `4bpp` packed in payload order - runtime memory may expand pixels to one `u8` palette index per pixel after decode -### 4.2 Layers +### 5.2 Layers - There are: - 4 Tile Layers @@ -129,7 +259,7 @@ The graphical world is composed of: --- -## 5. Internal Model of a Tile Layer +## 6. Internal Model of a Tile Layer A Tile Layer **is not a bitmap of pixels**. It is composed of: @@ -149,7 +279,7 @@ It is composed of: --- -## 6. Logical Tilemap +## 7. Logical Tilemap The tilemap represents the world: @@ -165,7 +295,7 @@ The tilemap can be much larger than the screen. --- -## 7. Border Cache (Tile Cache) +## 8. Border Cache (Tile Cache) The cache is a window of tiles around the camera. @@ -179,7 +309,7 @@ It stores tiles already resolved from the tilemap. --- -## 8. Cache Update +## 9. Cache Update Every frame: @@ -201,7 +331,7 @@ Only **one row and/or column** is updated per frame. --- -## 9. Cache as Ring Buffer +## 10. Cache as Ring Buffer The cache is circular: @@ -214,7 +344,7 @@ Access: --- -## 10. Canonical Game Projection +## 11. Canonical Game Projection Game mode uses a typed Game 2D submission. `composer.*` owns high-level Game 2D frame composition: scene binding, camera, sprites, HUD, and frame orchestration. @@ -243,7 +373,7 @@ uses `gfxui.*` and `ShellUiFramePacket`; it is never part of Game HUD or --- -## 11. Drawing Order and Priority +## 12. Drawing Order and Priority - There is no Z-buffer - There is no automatic sorting @@ -269,7 +399,7 @@ Normative boundary: --- -## 12. Transparency +## 13. Transparency Transparency is represented by the alpha channel of the resolved RGBA8888 color. @@ -289,7 +419,7 @@ else: --- -## 13. Color Math (Discrete Blending) +## 14. Color Math (Discrete Blending) Inspired by the SNES. @@ -313,7 +443,7 @@ Everything is: --- -## 14. Where Blend is Applied +## 15. Where Blend is Applied - Blending occurs during drawing - For canonical game composition, the result goes to the back buffer during composition @@ -323,7 +453,7 @@ Everything is: --- -## 15. What the GFX DOES NOT support +## 16. What the GFX DOES NOT support By design: @@ -338,7 +468,7 @@ By design: --- -## 16. Performance Rule +## 17. Performance Rule - Layers: - only update the border when crossing a tile @@ -350,7 +480,7 @@ By design: --- -## 17. Transitions and Removed Fade Contract +## 18. Transitions and Removed Fade Contract The previous special fade model is not part of the canonical render contract. Current render packets, syscalls, and ABI domains MUST NOT expose scene fade, @@ -371,9 +501,9 @@ Rules: --- -## 18. Palette System +## 19. Palette System -### 18.1. Overview +### 19.1. Overview PROMETEU uses **exclusively** palette-indexed graphics. @@ -382,7 +512,7 @@ Every graphical pixel is an **index** pointing to a real color in a palette. --- -### 18.2. Pixel Format +### 19.2. Pixel Format Each pixel of a tile or sprite is: @@ -396,7 +526,7 @@ Fixed rule: --- -### 18.3. Palette Structure +### 19.3. Palette Structure Each **Tile Bank** contains: @@ -413,7 +543,7 @@ Size: --- -### 18.4. Palette Association +### 19.4. Palette Association #### Fundamental Rule @@ -425,7 +555,7 @@ There is no palette swap within the same tile or sprite. --- -### 18.5. Where the Palette is Defined +### 19.5. Where the Palette is Defined #### Tilemap @@ -457,7 +587,7 @@ Runtime-facing validity rule for v1: --- -### 18.6. Color Resolution +### 19.6. Color Resolution The pipeline works like this: @@ -482,7 +612,7 @@ else: --- -### 18.7. Organization of Tile Banks +### 19.7. Organization of Tile Banks Tile Banks are "strong assets": @@ -507,7 +637,7 @@ Runtime-facing v1 baseline: --- -### 18.8. Metrics for Certification (CAP) +### 19.8. Metrics for Certification (CAP) The system can measure: @@ -518,7 +648,7 @@ The system can measure: --- -## 19. Syscall Return and Fault Policy +## 20. Syscall Return and Fault Policy Graphics-related public ABI in v1 is split between: @@ -538,7 +668,7 @@ Fault boundary: - `status`: operational failure; - `Panic`: internal runtime invariant break only. -### 19.1 Return-shape matrix in v1 +### 20.1 Return-shape matrix in v1 | Syscall | Return | Policy basis | | ----------------------- | ------------- | --------------------------------------------------- | @@ -555,7 +685,7 @@ Fault boundary: | `composer.set_camera` | `void` | no real operational failure path in v1 | | `composer.emit_sprite` | `status:int` | explicit orchestration-domain operational rejection | -### 19.1.a Primitive domain semantics +### 20.1.a Primitive domain semantics The primitive domains have stable operational meaning: @@ -569,7 +699,7 @@ Callers MUST NOT rely on stable immediate writes to the working back buffer as the public contract for primitive drawing. Primitive calls mutate domain command buffers that close into the active typed submission. -### 19.1.b Scene dependency fatal boundary +### 20.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. @@ -581,7 +711,7 @@ Rules: - 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` +### 20.2 `composer.emit_sprite` `composer.emit_sprite` returns `status:int`. From 8c43d85ce87ca67e92ae02d0db63426d840d7af4 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:52:46 +0100 Subject: [PATCH 11/47] implements PLN-0095 --- .../src/services/vm_runtime/lifecycle.rs | 6 ++ .../src/services/vm_runtime/mod.rs | 3 +- .../src/services/vm_runtime/render_manager.rs | 74 +++++++++++++++++++ .../src/services/vm_runtime/tick.rs | 16 +++- discussion/index.ndjson | 2 +- ...rototype-local-render-worker-capability.md | 2 +- 6 files changed, 98 insertions(+), 5 deletions(-) diff --git a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs index 7d64c02f..71d26ba5 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs @@ -23,6 +23,7 @@ impl VirtualMachineRuntime { current_cartridge_app_version: String::new(), current_cartridge_app_mode: AppMode::Game, frame_scheduler: FrameScheduler::default(), + render_capabilities: RenderRuntimeCapabilities::default(), render_manager: RenderManager::new(AppMode::Game), gfx2d_commands: Vec::new(), gfxui_commands: Vec::new(), @@ -121,6 +122,7 @@ impl VirtualMachineRuntime { self.current_cartridge_app_version.clear(); self.current_cartridge_app_mode = AppMode::Game; self.frame_scheduler = FrameScheduler::default(); + self.render_capabilities = RenderRuntimeCapabilities::default(); self.render_manager = RenderManager::new(AppMode::Game); self.gfx2d_commands.clear(); self.gfxui_commands.clear(); @@ -141,6 +143,10 @@ impl VirtualMachineRuntime { self.clear_cartridge_state(); } + pub fn set_render_runtime_capabilities(&mut self, capabilities: RenderRuntimeCapabilities) { + self.render_capabilities = capabilities; + } + pub fn initialize_vm( &mut self, log_service: &mut LogService, diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index c6b9ec44..4c889889 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -14,7 +14,7 @@ use prometeu_hal::log::LogService; use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier}; use prometeu_hal::{Gfx2dCommand, GfxUiCommand}; use prometeu_vm::VirtualMachine; -pub use render_manager::RenderManager; +pub use render_manager::{RenderManager, RenderRuntimeCapabilities}; use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::AtomicU32; @@ -31,6 +31,7 @@ pub struct VirtualMachineRuntime { pub current_cartridge_app_version: String, pub current_cartridge_app_mode: AppMode, pub frame_scheduler: FrameScheduler, + pub render_capabilities: RenderRuntimeCapabilities, pub render_manager: RenderManager, pub gfx2d_commands: Vec, pub gfxui_commands: Vec, diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs index 6241e955..f36b9833 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs @@ -29,6 +29,17 @@ pub enum RenderExecutionPolicy { WorkerCapableWithLocalFallback, } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct RenderRuntimeCapabilities { + pub game_render_worker: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderConsumerPath { + LocalSynchronous, + LocalWorkerPrototype, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RenderPolicy { pub app_mode: AppMode, @@ -55,12 +66,37 @@ impl RenderPolicy { pub const fn uses_frame_scheduler(self) -> bool { matches!(self.pacing, RenderPacingPolicy::FrameScheduled) } + + pub const fn resolve_consumer_path( + self, + capabilities: RenderRuntimeCapabilities, + ) -> RenderConsumerPath { + match (self.execution, capabilities.game_render_worker) { + (RenderExecutionPolicy::WorkerCapableWithLocalFallback, true) => { + RenderConsumerPath::LocalWorkerPrototype + } + _ => RenderConsumerPath::LocalSynchronous, + } + } } pub trait RenderSurface { fn consume_submission(&mut self, submission: &RenderSubmission); } +#[derive(Debug, Default)] +pub struct LocalRenderWorker; + +impl LocalRenderWorker { + pub fn consume_latest( + &mut self, + manager: &mut RenderManager, + surface: &mut S, + ) -> bool { + manager.publish_latest(surface) + } +} + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct RenderTelemetrySnapshot { pub produced_submissions: u64, @@ -499,6 +535,30 @@ mod tests { assert!(!policy.uses_frame_scheduler()); } + #[test] + fn render_policy_resolves_game_worker_only_when_capability_is_enabled() { + let policy = RenderPolicy::for_app_mode(AppMode::Game); + + assert_eq!( + policy.resolve_consumer_path(RenderRuntimeCapabilities::default()), + RenderConsumerPath::LocalSynchronous + ); + assert_eq!( + policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }), + RenderConsumerPath::LocalWorkerPrototype + ); + } + + #[test] + fn render_policy_keeps_shell_local_even_when_worker_capability_is_enabled() { + let policy = RenderPolicy::for_app_mode(AppMode::Shell); + + assert_eq!( + policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }), + RenderConsumerPath::LocalSynchronous + ); + } + #[test] fn active_render_policy_follows_app_mode_transition() { let mut manager = RenderManager::new(AppMode::Game); @@ -522,6 +582,20 @@ mod tests { assert_eq!(surface.seen, vec![FrameId::new(0)]); } + #[test] + fn local_render_worker_consumes_latest_game_submission() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + let mut worker = LocalRenderWorker; + + manager.close_compat_frame(); + assert!(worker.consume_latest(&mut manager, &mut surface)); + + assert_eq!(surface.seen, vec![FrameId::ZERO]); + assert_eq!(manager.render_telemetry().consumed_submissions, 1); + assert_eq!(manager.render_telemetry().presented_frames, 1); + } + #[test] fn render_handoff_takes_owned_latest_submission() { let mut handoff = RenderHandoff::default(); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index f4aae550..8624f248 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -1,5 +1,5 @@ use super::dispatch::VmRuntimeHost; -use super::render_manager::RenderSurface; +use super::render_manager::{LocalRenderWorker, RenderConsumerPath, RenderSurface}; use super::*; use crate::CrashReport; use crate::fs::{FsState, VirtualFS}; @@ -261,7 +261,19 @@ impl VirtualMachineRuntime { if let Err(payload) = catch_unwind(AssertUnwindSafe(|| { let mut surface = HardwareRenderSurface { hw }; - self.render_manager.publish_latest(&mut surface); + match self + .render_manager + .active_render_policy() + .resolve_consumer_path(self.render_capabilities) + { + RenderConsumerPath::LocalSynchronous => { + self.render_manager.publish_latest(&mut surface); + } + RenderConsumerPath::LocalWorkerPrototype => { + let mut worker = LocalRenderWorker; + worker.consume_latest(&mut self.render_manager, &mut surface); + } + } })) { let message = Self::host_panic_payload_to_string(payload); let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) }; diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 475939ef..896370e9 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md b/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md index 48a67003..7d0b9d23 100644 --- a/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md +++ b/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md @@ -2,7 +2,7 @@ id: PLN-0095 ticket: vm-render-parallel-execution-boundary title: Prototype Local Render Worker Capability -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, host, renderer, worker, prototype] From e4c1390614e7d4bba322666a91c0b6492973a34b Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:56:14 +0100 Subject: [PATCH 12/47] implements PLN-0096 --- .../src/services/vm_runtime/render_manager.rs | 160 +++++++++++++++++- .../src/services/vm_runtime/tick.rs | 39 +++-- discussion/index.ndjson | 2 +- ...ne-render-shutdown-and-failure-handling.md | 2 +- 4 files changed, 181 insertions(+), 22 deletions(-) diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs index f36b9833..613acc41 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs @@ -4,6 +4,7 @@ use prometeu_hal::{ FrameId, Game2DFramePacket, RenderOwnership, RenderSubmission, RenderSubmissionPacket, ShellUiFramePacket, }; +use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::Ordering; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -17,6 +18,27 @@ pub enum RenderSubmissionError { PacketAppModeMismatch { active: AppMode }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderConsumerState { + Running, + Stopped, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RenderConsumeOutcome { + NoSubmission, + Presented { frame_id: FrameId }, + DiscardedStaleEpoch { frame_id: FrameId }, + DiscardedShutdown { frame_id: FrameId }, + PresentFailed { frame_id: FrameId, message: String }, +} + +impl RenderConsumeOutcome { + pub fn presented(&self) -> bool { + matches!(self, Self::Presented { .. }) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RenderPacingPolicy { FrameScheduled, @@ -92,8 +114,8 @@ impl LocalRenderWorker { &mut self, manager: &mut RenderManager, surface: &mut S, - ) -> bool { - manager.publish_latest(surface) + ) -> RenderConsumeOutcome { + manager.consume_latest(surface) } } @@ -237,6 +259,7 @@ pub struct RenderManager { handoff: RenderHandoff, telemetry: RenderTelemetry, transition_state: RenderTransitionState, + consumer_state: RenderConsumerState, } impl RenderManager { @@ -250,6 +273,7 @@ impl RenderManager { handoff: RenderHandoff::default(), telemetry: RenderTelemetry::default(), transition_state: RenderTransitionState::Idle, + consumer_state: RenderConsumerState::Running, } } @@ -273,6 +297,10 @@ impl RenderManager { self.transition_state } + pub fn consumer_state(&self) -> RenderConsumerState { + self.consumer_state + } + pub fn latest_complete_submission(&self) -> Option<&RenderSubmission> { self.handoff.latest_submission() } @@ -301,6 +329,15 @@ impl RenderManager { discarded } + pub fn request_shutdown(&mut self) -> Option { + self.consumer_state = RenderConsumerState::Stopped; + self.discard_pending_submission() + } + + pub fn resume_consumer(&mut self) { + self.consumer_state = RenderConsumerState::Running; + } + pub fn render_telemetry(&self) -> RenderTelemetrySnapshot { let mut snapshot = self.telemetry.snapshot; snapshot.active_render_epoch = self.active_ownership.epoch; @@ -342,6 +379,16 @@ impl RenderManager { self.telemetry.record_present_error(frame_id); } + 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(); + } + "render panic without string payload".to_string() + } + pub fn set_active_app_mode(&mut self, app_mode: AppMode) { self.transition_render_owner(app_mode, self.active_app_id); } @@ -400,20 +447,43 @@ impl RenderManager { self.close_frame_with_packet(packet).expect("compat packet matches active app mode") } - pub fn publish_latest(&mut self, surface: &mut S) -> bool { + pub fn consume_latest(&mut self, surface: &mut S) -> RenderConsumeOutcome { + if self.consumer_state == RenderConsumerState::Stopped { + if let Some(discarded) = self.discard_pending_submission() { + return RenderConsumeOutcome::DiscardedShutdown { frame_id: discarded.frame_id }; + } + return RenderConsumeOutcome::NoSubmission; + } + let Some(submission) = self.handoff.take_latest() else { - return false; + return RenderConsumeOutcome::NoSubmission; }; if submission.ownership != self.active_ownership { self.telemetry.record_stale_epoch_discard(submission.frame_id); + let frame_id = submission.frame_id; self.handoff.record_stale_epoch_discard(submission); - return false; + return RenderConsumeOutcome::DiscardedStaleEpoch { frame_id }; } self.telemetry.record_consumed(submission.frame_id); - surface.consume_submission(&submission); + let present_result = + catch_unwind(AssertUnwindSafe(|| surface.consume_submission(&submission))); + if let Err(payload) = present_result { + let frame_id = submission.frame_id; + self.telemetry.record_present_error(frame_id); + self.handoff.record_consumed(submission); + return RenderConsumeOutcome::PresentFailed { + frame_id, + message: Self::panic_payload_to_string(payload), + }; + } self.telemetry.record_presented(submission.frame_id); + let frame_id = submission.frame_id; self.handoff.record_consumed(submission); - true + RenderConsumeOutcome::Presented { frame_id } + } + + pub fn publish_latest(&mut self, surface: &mut S) -> bool { + self.consume_latest(surface).presented() } } @@ -440,6 +510,15 @@ mod tests { } } + #[derive(Default)] + struct PanickingSurface; + + impl RenderSurface for PanickingSurface { + fn consume_submission(&mut self, _submission: &RenderSubmission) { + panic!("present backend failed"); + } + } + #[test] fn frame_closure_assigns_monotonic_frame_ids() { let mut manager = RenderManager::new(AppMode::Game); @@ -589,13 +668,78 @@ mod tests { let mut worker = LocalRenderWorker; manager.close_compat_frame(); - assert!(worker.consume_latest(&mut manager, &mut surface)); + assert!(worker.consume_latest(&mut manager, &mut surface).presented()); assert_eq!(surface.seen, vec![FrameId::ZERO]); assert_eq!(manager.render_telemetry().consumed_submissions, 1); assert_eq!(manager.render_telemetry().presented_frames, 1); } + #[test] + fn request_shutdown_discards_pending_submission_without_presenting() { + let mut manager = RenderManager::new(AppMode::Game); + + manager.close_compat_frame(); + let discarded = manager.request_shutdown().expect("pending submission discarded"); + + assert_eq!(discarded.frame_id, FrameId::ZERO); + assert_eq!(manager.consumer_state(), RenderConsumerState::Stopped); + assert!(manager.pending_submission().is_none()); + assert_eq!(manager.discarded_pending(), 1); + assert_eq!(manager.render_telemetry().shutdown_discards, 1); + assert_eq!(manager.render_telemetry().last_dropped_frame_id, 0); + } + + #[test] + fn stopped_consumer_discards_new_pending_submission_before_present() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + + manager.request_shutdown(); + manager.close_compat_frame(); + + let outcome = manager.consume_latest(&mut surface); + + assert_eq!(outcome, RenderConsumeOutcome::DiscardedShutdown { frame_id: FrameId::ZERO }); + assert!(surface.seen.is_empty()); + assert_eq!(manager.render_telemetry().shutdown_discards, 1); + assert_eq!(manager.render_telemetry().presented_frames, 0); + } + + #[test] + fn resume_consumer_allows_presentation_after_shutdown() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + + manager.request_shutdown(); + manager.resume_consumer(); + manager.close_compat_frame(); + + assert!(manager.consume_latest(&mut surface).presented()); + assert_eq!(surface.seen, vec![FrameId::ZERO]); + } + + #[test] + fn present_panic_is_reported_without_unwinding_to_vm() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = PanickingSurface; + + manager.close_compat_frame(); + let outcome = manager.consume_latest(&mut surface); + + assert_eq!( + outcome, + RenderConsumeOutcome::PresentFailed { + frame_id: FrameId::ZERO, + message: "present backend failed".to_string() + } + ); + assert_eq!(manager.render_telemetry().consumed_submissions, 1); + assert_eq!(manager.render_telemetry().present_errors, 1); + assert_eq!(manager.render_telemetry().presented_frames, 0); + assert_eq!(manager.render_telemetry().last_error_frame_id, 0); + } + #[test] fn render_handoff_takes_owned_latest_submission() { let mut handoff = RenderHandoff::default(); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index 8624f248..a09672e8 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -1,5 +1,7 @@ use super::dispatch::VmRuntimeHost; -use super::render_manager::{LocalRenderWorker, RenderConsumerPath, RenderSurface}; +use super::render_manager::{ + LocalRenderWorker, RenderConsumeOutcome, RenderConsumerPath, RenderSurface, +}; use super::*; use crate::CrashReport; use crate::fs::{FsState, VirtualFS}; @@ -7,7 +9,7 @@ use crate::services::memcard::MemcardService; use prometeu_hal::asset::{BankTelemetry, BankType}; use prometeu_hal::log::{LogLevel, LogService, LogSource}; use prometeu_hal::{ - HardwareBridge, HostContext, InputSignals, RenderSubmission, RenderSubmissionPacket, + FrameId, HardwareBridge, HostContext, InputSignals, RenderSubmission, RenderSubmissionPacket, ShellUiFramePacket, }; use prometeu_vm::LogicalFrameEndingReason; @@ -259,7 +261,7 @@ impl VirtualMachineRuntime { .expect("shell packet must match Shell app mode"); } - if let Err(payload) = catch_unwind(AssertUnwindSafe(|| { + let render_outcome = catch_unwind(AssertUnwindSafe(|| { let mut surface = HardwareRenderSurface { hw }; match self .render_manager @@ -267,25 +269,38 @@ impl VirtualMachineRuntime { .resolve_consumer_path(self.render_capabilities) { RenderConsumerPath::LocalSynchronous => { - self.render_manager.publish_latest(&mut surface); + self.render_manager.consume_latest(&mut surface) } RenderConsumerPath::LocalWorkerPrototype => { let mut worker = LocalRenderWorker; - worker.consume_latest(&mut self.render_manager, &mut surface); + worker.consume_latest(&mut self.render_manager, &mut surface) } } - })) { - let message = Self::host_panic_payload_to_string(payload); - let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) }; + })); + + let render_outcome = match render_outcome { + Ok(outcome) => outcome, + Err(payload) => { + let message = Self::host_panic_payload_to_string(payload); + let frame_id = self + .render_manager + .pending_submission() + .map(|submission| submission.frame_id) + .unwrap_or(FrameId::ZERO); + self.render_manager.record_render_error(frame_id); + RenderConsumeOutcome::PresentFailed { frame_id, message } + } + }; + + if let RenderConsumeOutcome::PresentFailed { message, .. } = &render_outcome + { self.log( log_service, LogLevel::Error, LogSource::Vm, - report.log_tag(), - report.summary(), + 0, + format!("Render publication failed: {}", message), ); - self.last_crash_report = Some(report.clone()); - return Some(report); } // 1. Snapshot full telemetry at logical frame end diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 896370e9..d9ab7c35 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md b/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md index 03cd52db..b6a45125 100644 --- a/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md +++ b/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md @@ -2,7 +2,7 @@ id: PLN-0096 ticket: vm-render-parallel-execution-boundary title: Define Render Shutdown and Failure Handling -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, renderer, shutdown, errors] From 8cd218dc04d9855b12bb65739d5e24f4730c7bc0 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 5 Jun 2026 09:58:41 +0100 Subject: [PATCH 13/47] implements PLN-0097 --- .../vm_runtime/async_render_contract_tests.rs | 236 ++++++++++++++++++ .../src/services/vm_runtime/mod.rs | 2 + discussion/index.ndjson | 2 +- ...dd-async-render-integration-test-matrix.md | 2 +- 4 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 crates/console/prometeu-system/src/services/vm_runtime/async_render_contract_tests.rs diff --git a/crates/console/prometeu-system/src/services/vm_runtime/async_render_contract_tests.rs b/crates/console/prometeu-system/src/services/vm_runtime/async_render_contract_tests.rs new file mode 100644 index 00000000..bc145990 --- /dev/null +++ b/crates/console/prometeu-system/src/services/vm_runtime/async_render_contract_tests.rs @@ -0,0 +1,236 @@ +use super::FrameScheduler; +use super::frame_scheduler::FrameAuthorizationError; +use super::render_manager::{ + LocalRenderWorker, RenderConsumeOutcome, RenderConsumerPath, RenderExecutionPolicy, + RenderPacingPolicy, RenderPolicy, RenderRuntimeCapabilities, RenderSurface, +}; +use prometeu_hal::app_mode::AppMode; +use prometeu_hal::telemetry::AtomicTelemetry; +use prometeu_hal::{ + FrameId, Game2DFramePacket, RenderOwnership, RenderSubmission, RenderSubmissionPacket, + ShellUiFramePacket, +}; +use std::sync::Arc; +use std::sync::atomic::AtomicU32; + +use super::render_manager::RenderManager; + +struct ContractMatrixRow { + invariant: &'static str, + validations: &'static [&'static str], +} + +const ASYNC_RENDER_CONTRACT_MATRIX: &[ContractMatrixRow] = &[ + ContractMatrixRow { + invariant: "complete packet and AppMode policy are explicit", + validations: &["matrix_packet_policy_and_pacing_are_mode_specific"], + }, + ContractMatrixRow { + invariant: "handoff is single-slot latest-wins in sync and worker-capable paths", + validations: &["matrix_sync_and_worker_paths_share_latest_wins_handoff_contract"], + }, + ContractMatrixRow { + invariant: "render consumers see resource banks through read-only access", + validations: &[ + "prometeu-drivers::render_resource_access_traits_are_thread_safe", + "prometeu-drivers::render_resource_access_exposes_read_only_glyph_and_scene_banks", + ], + }, + ContractMatrixRow { + invariant: "Game pacing is frame scheduled and does not catch up skipped frames", + validations: &["matrix_packet_policy_and_pacing_are_mode_specific"], + }, + ContractMatrixRow { + invariant: "ownership epoch prevents obsolete work from presenting", + validations: &["matrix_epoch_stale_discard_and_telemetry_sync_remain_consistent"], + }, + ContractMatrixRow { + invariant: "telemetry exposes production, drops, consumes, presents, repeats, and errors", + validations: &[ + "matrix_epoch_stale_discard_and_telemetry_sync_remain_consistent", + "matrix_shutdown_failure_and_repeat_are_observable", + ], + }, + ContractMatrixRow { + invariant: "shutdown discards pending work and bounded local worker failure is contained", + validations: &["matrix_shutdown_failure_and_repeat_are_observable"], + }, + ContractMatrixRow { + invariant: "active-scene Game2D overlay is composed after layer blit", + validations: &[ + "prometeu-drivers::publish_game2d_submission_applies_gfx2d_overlay_after_active_scene", + ], + }, +]; + +#[derive(Default)] +struct RecordingSurface { + seen: Vec, +} + +impl RenderSurface for RecordingSurface { + fn consume_submission(&mut self, submission: &RenderSubmission) { + self.seen.push(submission.frame_id); + } +} + +struct PanickingSurface; + +impl RenderSurface for PanickingSurface { + fn consume_submission(&mut self, _submission: &RenderSubmission) { + panic!("matrix present failure"); + } +} + +fn close_game(manager: &mut RenderManager) { + manager + .close_frame_with_packet(RenderSubmissionPacket::Game2D(Game2DFramePacket::default())) + .expect("game packet must match Game mode"); +} + +fn close_shell(manager: &mut RenderManager) { + manager + .close_frame_with_packet(RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new( + Vec::new(), + ))) + .expect("shell packet must match Shell mode"); +} + +#[test] +fn async_render_contract_matrix_rows_have_validation() { + assert!(ASYNC_RENDER_CONTRACT_MATRIX.len() >= 7); + for row in ASYNC_RENDER_CONTRACT_MATRIX { + assert!(!row.invariant.is_empty()); + assert!(!row.validations.is_empty(), "missing validation for {}", row.invariant); + } +} + +#[test] +fn matrix_packet_policy_and_pacing_are_mode_specific() { + let game_policy = RenderPolicy::for_app_mode(AppMode::Game); + assert_eq!(game_policy.pacing, RenderPacingPolicy::FrameScheduled); + assert_eq!(game_policy.execution, RenderExecutionPolicy::WorkerCapableWithLocalFallback); + assert_eq!( + game_policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }), + RenderConsumerPath::LocalWorkerPrototype + ); + + let shell_policy = RenderPolicy::for_app_mode(AppMode::Shell); + assert_eq!(shell_policy.pacing, RenderPacingPolicy::LifecycleDriven); + assert_eq!(shell_policy.execution, RenderExecutionPolicy::LocalSynchronous); + assert_eq!( + shell_policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }), + RenderConsumerPath::LocalSynchronous + ); + + let mut game_manager = RenderManager::new(AppMode::Game); + close_game(&mut game_manager); + assert!( + game_manager + .close_frame_with_packet(RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new( + Vec::new() + ))) + .is_err() + ); + + let mut shell_manager = RenderManager::new(AppMode::Shell); + close_shell(&mut shell_manager); + assert!( + shell_manager + .close_frame_with_packet(RenderSubmissionPacket::Game2D(Game2DFramePacket::default())) + .is_err() + ); + + let mut scheduler = FrameScheduler::default(); + assert_eq!(scheduler.authorize_game_frame(), Ok(FrameId::ZERO)); + scheduler.record_logical_frame_overrun(); + assert_eq!( + scheduler.authorize_game_frame(), + Err(FrameAuthorizationError::GameFrameAlreadyActive { active_frame_id: FrameId::ZERO }) + ); + assert_eq!(scheduler.complete_game_frame(), Some(FrameId::ZERO)); + assert_eq!(scheduler.authorize_game_frame(), Ok(FrameId::new(1))); +} + +#[test] +fn matrix_sync_and_worker_paths_share_latest_wins_handoff_contract() { + fn consume_with(path: RenderConsumerPath) -> (RenderConsumeOutcome, Vec, u64) { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + + close_game(&mut manager); + close_game(&mut manager); + + let outcome = match path { + RenderConsumerPath::LocalSynchronous => manager.consume_latest(&mut surface), + RenderConsumerPath::LocalWorkerPrototype => { + let mut worker = LocalRenderWorker; + worker.consume_latest(&mut manager, &mut surface) + } + }; + + (outcome, surface.seen, manager.render_telemetry().replaced_before_consume) + } + + for path in [RenderConsumerPath::LocalSynchronous, RenderConsumerPath::LocalWorkerPrototype] { + let (outcome, seen, replaced) = consume_with(path); + assert_eq!(outcome, RenderConsumeOutcome::Presented { frame_id: FrameId::new(1) }); + assert_eq!(seen, vec![FrameId::new(1)]); + assert_eq!(replaced, 1); + } +} + +#[test] +fn matrix_epoch_stale_discard_and_telemetry_sync_remain_consistent() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + let atomic = AtomicTelemetry::new(Arc::new(AtomicU32::new(0))); + + manager.transition_render_owner(AppMode::Game, 7); + close_game(&mut manager); + manager.transition_render_owner(AppMode::Game, 8); + + assert_eq!( + manager.consume_latest(&mut surface), + RenderConsumeOutcome::DiscardedStaleEpoch { frame_id: FrameId::ZERO } + ); + assert!(surface.seen.is_empty()); + + manager.sync_telemetry(&atomic); + let snapshot = atomic.snapshot(); + assert_eq!(snapshot.produced_submissions, 1); + assert_eq!(snapshot.stale_epoch_discards, 1); + assert_eq!(snapshot.presented_frames, 0); + assert_eq!(snapshot.last_dropped_frame_id, 0); + assert_eq!(snapshot.active_render_epoch, 2); + assert_eq!(manager.active_ownership(), RenderOwnership::new(2, AppMode::Game, 8)); +} + +#[test] +fn matrix_shutdown_failure_and_repeat_are_observable() { + let mut manager = RenderManager::new(AppMode::Game); + + close_game(&mut manager); + let discarded = manager.request_shutdown().expect("pending frame discarded"); + assert_eq!(discarded.frame_id, FrameId::ZERO); + + manager.resume_consumer(); + close_game(&mut manager); + let mut surface = PanickingSurface; + assert_eq!( + manager.consume_latest(&mut surface), + RenderConsumeOutcome::PresentFailed { + frame_id: FrameId::new(1), + message: "matrix present failure".to_string() + } + ); + manager.record_repeated_present(FrameId::new(1)); + + let telemetry = manager.render_telemetry(); + assert_eq!(telemetry.shutdown_discards, 1); + assert_eq!(telemetry.present_errors, 1); + assert_eq!(telemetry.presented_frames, 0); + assert_eq!(telemetry.repeated_presents, 1); + assert_eq!(telemetry.last_error_frame_id, 1); + assert_eq!(telemetry.last_presented_frame_id, 1); +} diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index 4c889889..a522bb4f 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod async_render_contract_tests; mod dispatch; mod frame_scheduler; mod lifecycle; diff --git a/discussion/index.ndjson b/discussion/index.ndjson index d9ab7c35..2af89f18 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md b/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md index baecaef0..18e007ea 100644 --- a/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md +++ b/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md @@ -2,7 +2,7 @@ id: PLN-0097 ticket: vm-render-parallel-execution-boundary title: Add Async Render Integration Test Matrix -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, renderer, tests, integration] From df0d7949c8aff2a437a8e230a12ad1101f3a1af4 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 6 Jun 2026 11:53:48 +0100 Subject: [PATCH 14/47] fix stress render packet scene composition --- .../prometeu-drivers/src/frame_composer.rs | 49 ++++++++++++++++++ .../console/prometeu-drivers/src/hardware.rs | 46 ++++++++++++++-- crates/tools/pbxgen-stress/src/lib.rs | 25 +++------ test-cartridges/stress-console/program.pbx | Bin 1205 -> 1173 bytes 4 files changed, 100 insertions(+), 20 deletions(-) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index bdbdebc9..dd95bfda 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -327,6 +327,55 @@ impl FrameComposer { gfx.render_no_scene_frame(); } + pub fn render_game2d_frame_packet( + &mut self, + packet: &Game2DFramePacket, + gfx: &mut dyn GfxBridge, + ) { + let sprites: Vec = + packet.composer.sprites.iter().map(Self::sprite_from_packet).collect(); + gfx.load_frame_sprites(&sprites); + + let Some(bound_scene) = packet.composer.bound_scene else { + gfx.render_no_scene_frame(); + return; + }; + + let scene_bank_id = bound_scene.bank_id as usize; + if self.active_scene_id != Some(scene_bank_id) && !self.bind_scene(scene_bank_id) { + gfx.render_no_scene_frame(); + return; + } + + self.camera_x_px = packet.composer.camera.x; + self.camera_y_px = packet.composer.camera.y; + + let scene = + self.active_scene.as_deref().expect("active scene should exist after packet bind"); + let resolved_glyph_slots = self.resolve_glyph_slots(scene, scene_bank_id, "render_packet"); + let (Some(cache), Some(resolver)) = (self.cache.as_mut(), self.resolver.as_mut()) else { + panic!("SCENE runtime invariant broken: packet scene without cache/resolver"); + }; + let update = resolver.update(scene, self.camera_x_px, self.camera_y_px); + Self::sync_cache_windows(cache, &update); + Self::apply_refresh_requests(cache, scene, &update.refresh_requests); + gfx.render_scene_from_cache(cache, &update, &resolved_glyph_slots); + } + + fn sprite_from_packet(sprite: &GameSpritePacket) -> Sprite { + Sprite { + glyph: Glyph { glyph_id: sprite.glyph_id as u16, palette_id: sprite.palette_id as u8 }, + x: sprite.x, + y: sprite.y, + layer: sprite.layer as u8, + bank_id: sprite.bank_id as u8, + active: true, + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + priority: sprite.priority as u8, + } + } + fn build_scene_runtime( viewport_width_px: usize, viewport_height_px: usize, diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 77ff7dd8..c04868cc 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -75,10 +75,10 @@ impl HardwareBridge for Hardware { fn publish_render_submission(&mut self, submission: &RenderSubmission) { match &submission.packet { RenderSubmissionPacket::Game2D(packet) => { - if self.frame_composer.active_scene_id().is_none() { + if packet.composer.bound_scene.is_none() { self.gfx.render_game2d_frame_packet(packet); } else { - self.frame_composer.render_frame(&mut self.gfx); + self.frame_composer.render_game2d_frame_packet(packet, &mut self.gfx); self.gfx.apply_gfx2d_commands(&packet.gfx2d); } } @@ -195,7 +195,9 @@ mod tests { use prometeu_hal::scene_viewport_resolver::SceneViewportResolver; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; - use prometeu_hal::{FrameId, Gfx2dCommand}; + use prometeu_hal::{ + BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, GameSpritePacket, Gfx2dCommand, + }; fn make_glyph_bank() -> GlyphBank { let mut bank = GlyphBank::new(TileSize::Size8, 8, 8); @@ -292,4 +294,42 @@ mod tests { assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); } + + #[test] + fn publish_game2d_submission_uses_packet_scene_and_sprites_not_live_frame_state() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(make_glyph_bank())); + banks.install_scene_bank(0, Arc::new(make_scene_with_glyph_asset_id(0))); + + let mut hardware = Hardware::new_with_memory_banks(banks); + let mut glyph_slots = [None; 16]; + glyph_slots[0] = Some(0); + hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots); + assert!(hardware.bind_scene(0)); + + let packet = Game2DFramePacket::new( + ComposerFramePacket { + bound_scene: Some(BoundScenePacket { bank_id: 0 }), + camera: Camera2D::default(), + sprites: vec![GameSpritePacket { + glyph_id: 0, + palette_id: 0, + x: 0, + y: 0, + layer: 0, + bank_id: 0, + flip_x: false, + flip_y: false, + priority: 0, + }], + hud: Default::default(), + }, + Vec::new(), + ); + + hardware.begin_frame(); + hardware.publish_render_submission(&RenderSubmission::game2d(FrameId::ZERO, packet)); + + assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw()); + } } diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index cb00152f..11b8bee1 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -41,13 +41,6 @@ fn asm(s: &str) -> Vec { pub fn generate() -> Result<()> { let mut rom: Vec = Vec::new(); let syscalls = vec![ - SyscallDecl { - module: "gfx2d".into(), - name: "clear".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }, SyscallDecl { module: "gfx2d".into(), name: "draw_text".into(), @@ -143,17 +136,15 @@ fn heavy_load(rom: &mut Vec) { let jif_bind_done_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); - rom.extend(asm("PUSH_I32 0\nHOSTCALL 3\nPOP_N 1\nPUSH_I32 1\nSET_GLOBAL 1")); + rom.extend(asm("PUSH_I32 0\nHOSTCALL 2\nPOP_N 1\nPUSH_I32 1\nSET_GLOBAL 1")); let bind_done_target = rom.len() as u32; - rom.extend(asm("PUSH_I32 0\nHOSTCALL 0")); - let scene_width_px = (STRESS_SCENE_TILE_W * 8) as i32; let scene_height_px = (STRESS_SCENE_TILE_H * 8) as i32; let camera_x_span = scene_width_px - STRESS_VIEWPORT_W; let camera_y_span = scene_height_px - STRESS_VIEWPORT_H; rom.extend(asm(&format!( - "GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 {camera_x_span}\nMOD\nGET_GLOBAL 0\nPUSH_I32 {camera_y_span}\nMOD\nHOSTCALL 4" + "GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 {camera_x_span}\nMOD\nGET_GLOBAL 0\nPUSH_I32 {camera_y_span}\nMOD\nHOSTCALL 3" ))); rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0")); @@ -178,7 +169,7 @@ fn heavy_load(rom: &mut Vec) { GET_LOCAL 1\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\ GET_LOCAL 0\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\ GET_LOCAL 0\nGET_LOCAL 1\nADD\nPUSH_I32 4\nMOD\n\ - HOSTCALL 5\nPOP_N 1" + HOSTCALL 4\nPOP_N 1" ))); rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); @@ -195,27 +186,27 @@ fn heavy_load(rom: &mut Vec) { PUSH_I32 8\n\ PUSH_CONST 0\n\ GET_GLOBAL 0\nPUSH_I32 2047\nMUL\nPUSH_I32 65535\nBIT_AND\n\ - HOSTCALL 1")); + HOSTCALL 0")); rom.extend(asm("PUSH_I32 16\n\ GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 186\nMOD\nPUSH_I32 32\nADD\n\ PUSH_CONST 1\n\ GET_GLOBAL 0\nPUSH_I32 4093\nMUL\nPUSH_I32 65535\nBIT_AND\n\ - HOSTCALL 1")); + HOSTCALL 0")); rom.extend(asm("PUSH_I32 336\n\ GET_GLOBAL 0\nPUSH_I32 5\nMUL\nPUSH_I32 194\nMOD\n\ PUSH_CONST 2\n\ GET_GLOBAL 0\nPUSH_I32 1237\nMUL\nPUSH_I32 65535\nBIT_AND\n\ - HOSTCALL 1")); + HOSTCALL 0")); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 4\nMUL\nPUSH_I32 240\nMOD\nPUSH_I32 112\nADD\n\ GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 120\nMOD\nPUSH_I32 88\nADD\n\ PUSH_CONST 3\n\ GET_GLOBAL 0\nPUSH_I32 3001\nMUL\nPUSH_I32 65535\nBIT_AND\n\ - HOSTCALL 1")); + HOSTCALL 0")); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ")); let jif_log_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); - rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 2")); + rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 1")); let after_log = rom.len() as u32; rom.extend(asm("FRAME_SYNC\nRET")); diff --git a/test-cartridges/stress-console/program.pbx b/test-cartridges/stress-console/program.pbx index a06e0838fad5e6503e5232c06faa65e4b9febd9e..7191e3a6563d105b6ad8e027c493d5d69066c53a 100644 GIT binary patch delta 173 zcmdnWIhAvQgQ^2F0|O8-yaN&~!W> xeV};^4j_U-oWTZ2ssL#aFaSEi2Iv5B1|}d6WHkdrAp_&&&&=A4tdosdngAwo97q5F delta 190 zcmbQrxs`K*gQ^cR0|O8-`~ngpK$-=J`GAA~kY)v9IUsxb#8g{GpNWgD8JQ>E7v&TO z3NtViGB8a1sL9AOS(;Iv@z-Q)Ao*Z(G$S)3BkSaNCV9pclk1rz89OG=WU^&v1gaNj z5SaK;c=87(9Y)5 Date: Sat, 6 Jun 2026 12:03:10 +0100 Subject: [PATCH 15/47] VM and Render Parallel Execution Boundary --- discussion/index.ndjson | 5 +- ...r-workers-need-a-closed-packet-contract.md | 104 +++ ...-and-render-parallel-execution-boundary.md | 729 ------------------ ...D-0042-real-render-worker-establishment.md | 172 +++++ ...-and-render-parallel-execution-boundary.md | 245 ------ ...7-fix-game2d-packet-overlay-composition.md | 62 -- ...fine-read-only-render-resource-boundary.md | 63 -- ...89-introduce-render-handoff-abstraction.md | 62 -- ...N-0090-add-render-frame-pacing-contract.md | 63 -- ...-implement-appmode-render-policy-matrix.md | 63 -- ...-add-render-epoch-ownership-transitions.md | 64 -- ...dd-render-telemetry-counters-and-events.md | 66 -- .../PLN-0094-specify-async-render-boundary.md | 60 -- ...rototype-local-render-worker-capability.md | 66 -- ...ne-render-shutdown-and-failure-handling.md | 70 -- ...dd-async-render-integration-test-matrix.md | 61 -- 16 files changed, 279 insertions(+), 1676 deletions(-) create mode 100644 discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md delete mode 100644 discussion/workflow/agendas/AGD-0040-vm-and-render-parallel-execution-boundary.md create mode 100644 discussion/workflow/agendas/AGD-0042-real-render-worker-establishment.md delete mode 100644 discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md delete mode 100644 discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md delete mode 100644 discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md delete mode 100644 discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md delete mode 100644 discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md delete mode 100644 discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md delete mode 100644 discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md delete mode 100644 discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md delete mode 100644 discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md delete mode 100644 discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md delete mode 100644 discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md delete mode 100644 discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 2af89f18..852a7508 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,7 +1,8 @@ -{"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} +{"type":"meta","next_id":{"DSC":43,"AGD":43,"DEC":32,"PLN":98,"LSN":49,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"open","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-06","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-real-render-worker-establishment.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md b/discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md new file mode 100644 index 00000000..48001ed6 --- /dev/null +++ b/discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md @@ -0,0 +1,104 @@ +--- +id: LSN-0048 +ticket: vm-render-parallel-execution-boundary +title: Render Workers Need a Closed Packet Contract +created: 2026-06-06 +tags: [runtime, renderer, vm, concurrency, handoff, telemetry] +--- + +## Context + +`DSC-0040` took the runtime from a local "close and immediately render" model toward an architecture that can later support a real render worker, separate thread, separate core, or host-provided renderer. + +The important result was not a real worker thread. The result was the contract a real worker must obey: the VM/logical side produces closed render submissions; the render side consumes those submissions without relying on live mutable VM, `Hardware`, `Gfx`, or `FrameComposer` state. + +This work built on `DSC-0038`, where typed render submissions became the domain boundary. `DSC-0040` made that boundary operational by adding ownership, pacing, handoff, AppMode policy, shutdown semantics, and telemetry. + +## Key Decisions + +### Game Frames Are Closed Submissions + +**What:** `RenderSubmissionPacket::Game2D` is the render consumer's complete frame description. The consumer must compose scene/layers/sprites first and then apply `gfx2d` primitives as the final overlay. + +**Why:** A worker cannot safely reconstruct a frame from live runtime state. If an active-scene path bypasses the packet and calls a live `FrameComposer` path, the packet is no longer a true handoff boundary. + +**Trade-offs:** Keeping packets small means heavy resources are referenced by stable IDs rather than copied per frame. That requires disciplined resource lifetime and read-only access APIs. + +### Handoff Is Single-Slot Latest-Wins + +**What:** The producer writes one pending submission. A newer submission replaces an older unconsumed one, and the replacement is telemetry. + +**Why:** Render queues should not accumulate stale game frames. Latest-wins bounds memory, latency, and stale presentation risk. + +**Trade-offs:** The system does not guarantee that every produced frame is consumed or presented. VM semantics must not depend on present acknowledgement. + +### Frame Pacing Is Not Render ACK + +**What:** Game logical frames are paced by a frame scheduler, not by render completion. `FRAME_SYNC` remains the logical frame boundary. + +**Why:** Letting the worker decide when the VM may advance mixes display/raster completion with logical simulation authority. The VM should not run freely, but it also should not wait for a specific render ACK. + +**Trade-offs:** A delayed render consumer repeats the last valid frame, while logical frames remain sequential. Overruns are diagnostics and telemetry, not hidden catch-up simulation. + +### AppMode Owns Render Policy + +**What:** Game workloads are frame-paced and worker-capable with local fallback. Shell/System UI remains lifecycle-driven and local/synchronous by default. + +**Why:** A Shell app has a different lifecycle from a game loop. Treating Shell as just another game renderer would force unnecessary frame pacing and worker semantics into UI flows. + +**Trade-offs:** The packet boundary is shared, but execution policy differs by AppMode. Future Shell worker support should be a separate decision, not an accidental inheritance from Game. + +### Epoch Guards Presentation + +**What:** Submissions carry render ownership. Foreground visual-owner changes update the active render epoch, and stale submissions are discarded before present. + +**Why:** A worker may finish old work after the visible owner has changed. Without an epoch check, old Game, Shell, splash, or crash frames can be presented after they are no longer valid. + +**Trade-offs:** Epoch protects correctness, but it does not solve foreground stack policy. Game pause/resume and VM-backed Shell coexistence belong to lifecycle/SystemOS discussions. + +## Patterns and Algorithms + +The durable pattern is: + +```text +logical core / VM + -> close typed RenderSubmission + -> publish to single pending slot + -> render consumer takes owned submission + -> validate ownership/epoch + -> rasterize from packet + read-only resources + -> present or repeat last valid frame + -> report telemetry +``` + +The render manager is a coordinator, not a renderer. It owns policy, frame IDs, handoff, ownership, shutdown state, and telemetry. Concrete raster/present capability belongs to a render surface, backend, host, HAL, or future worker implementation. + +The current `LocalRenderWorker` is intentionally a prototype. It proves the same policy path can select a worker-capable consumer, but it does not prove thread ownership, bounded join, stop token behavior for in-flight work, or a real 60Hz present loop. + +## Pitfalls + +### Treating overlay commands as background setup + +Once `gfx2d` is a final overlay, `gfx2d.clear` in a scene-backed game frame clears over the composed scene and sprites. That is a cartridge/framework discipline issue: scene-backed games should not use final overlay clear as world background initialization. + +### Consulting live composer state during packet consumption + +The active-scene consumer must not decide from live `FrameComposer` state whether a packet has a scene. It must read `packet.composer.bound_scene`. Otherwise the worker boundary silently depends on mutable state that may already belong to a different logical frame. + +### Counting telemetry without behavior + +Counters such as repeated presents and shutdown discards are useful only if later worker work connects them to real display cadence and real in-flight cancellation. The local implementation establishes the surface; a real worker still needs behavior behind it. + +### Assuming read-only banks solve viewport cache + +Read-only glyph and scene bank access is only part of the worker boundary. The viewport cache and resolver path still need a concrete read-only or ownership model before a real threaded worker can rasterize without live `FrameComposer`. + +## Takeaways + +- A render worker is first a contract problem, then a threading problem. +- Closed packets must be complete enough to render without live VM/runtime state. +- Latest-wins handoff keeps render latency bounded and makes dropped frames observable. +- Game pacing belongs to the frame scheduler; render completion is telemetry, not VM semantics. +- Shell UI can share the packet concept without inheriting the Game frame loop. +- Epoch checks are the guardrail that prevents obsolete visual owners from presenting stale work. +- The real worker should be introduced as a separate discussion and plan, using this contract rather than reopening it. diff --git a/discussion/workflow/agendas/AGD-0040-vm-and-render-parallel-execution-boundary.md b/discussion/workflow/agendas/AGD-0040-vm-and-render-parallel-execution-boundary.md deleted file mode 100644 index bc908293..00000000 --- a/discussion/workflow/agendas/AGD-0040-vm-and-render-parallel-execution-boundary.md +++ /dev/null @@ -1,729 +0,0 @@ ---- -id: AGD-0040 -ticket: vm-render-parallel-execution-boundary -title: VM and Render Parallel Execution Boundary -status: accepted -created: 2026-06-04 -resolved: -decision: -tags: [runtime, renderer, vm, concurrency, architecture, perf] ---- - -# Agenda - VM and Render Parallel Execution Boundary - -## Contexto - -`DSC-0038` estabeleceu `RenderSubmission` como snapshot fechado. A lesson `LSN-0047` registra explicitamente que uma submissao fechada e o primitivo util para concorrencia futura, mesmo que o v1 ainda consuma tudo de forma sincrona. - -No codigo atual, `RenderManager` guarda apenas `latest_complete_submission`, fecha frames no fluxo da VM/runtime e publica para uma `RenderSurface` via chamada direta. Isso e correto para o v1, mas ainda nao decide como mover rasterizacao/publicacao para outra thread ou core. - -A demanda aqui e separar a VM continuando em um core/thread de execucao logica enquanto o pipeline grafico consome submissions fechadas em outro worker/core, sem acesso mutavel vivo a VM, `Hardware` ou `Gfx`. - -## Problema - -Prometeu ainda nao tem contrato operacional para paralelismo VM/render. - -A fronteira de dados ja aponta a direcao, mas faltam decisoes sobre: - -- quem cria e possui a fila ou slot de submissao; -- se a politica `latest complete submission wins` basta quando ha worker real; -- como lidar com frame pacing, backpressure e queda de frames; -- quais recursos podem ser compartilhados read-only; -- como manter determinismo da VM; -- como representar isso em desktop host, handheld proprio e testes; -- se o render worker e parte do runtime, do host, da HAL ou apenas uma abstracao futura. - -## Pontos Criticos - -### 1. A VM nao pode depender do consumidor de render - -A VM deve produzir estado/comandos e avançar segundo seu proprio budget. O render worker pode atrasar, pular frames ou publicar a ultima submissao completa, mas nao deve exigir acesso mutavel ao estado vivo da VM para rasterizar. - -### 2. Snapshot fechado nao e automaticamente thread-safe - -`RenderSubmission` owned ajuda, mas qualquer referencia indireta a assets, scene cache, bancos de glyphs ou surfaces precisa ter politica clara: copia, handle estavel, Arc/read-only, snapshot de recurso, ou acesso mediado. - -### 3. `latest complete submission wins` precisa virar protocolo - -Como principio, nao queremos fila infinita. Mas uma implementacao com worker precisa decidir se usa single-slot atomico, double/triple buffering, canal bounded, sequence numbers, acknowledgements ou outro mecanismo. - -### 4. Frame pacing muda quando render sai do fluxo principal - -Hoje tick, fechamento de frame, consumo e host presentation estao acoplados. Com worker separado, a VM pode produzir mais rapido que o render ou o render pode apresentar em cadencia diferente. Isso precisa de uma regra de tempo. - -### 5. Desktop thread nao e automaticamente modelo de handheld core - -No host desktop, a implementacao natural pode ser uma thread. No hardware proprio, pode ser outro core, DMA, coprocessador, scanout ou rotina cooperativa. A decisao deve separar contrato runtime de implementacao host. - -## Opcoes - -### Opcao A - Manter consumo sincrono, apenas preservar contrato de snapshot - -**Abordagem:** - -Nao criar worker agora. Consolidar somente as invariantes: submissions owned/fechadas, sem acesso mutavel vivo durante consumo, e testes que impedem regressao para immediate rendering. - -**Pro:** - -- menor risco; -- evita concorrencia prematura; -- mantem foco em estabilizar o novo renderer; -- ainda prepara a arquitetura. - -**Con:** - -- nao reduz latencia/custo no curto prazo; -- pode adiar decisoes dificeis sobre recursos compartilhados; -- o codigo pode voltar a assumir consumo sincrono se nao houver guardrails. - -**Maintainability:** - -Alta como etapa intermediaria, mas incompleta para a demanda de core/thread separado. - -### Opcao B - Render worker com slot bounded `latest-complete` - -**Abordagem:** - -Criar um worker de render que consome um slot/canal bounded de `RenderSubmission`. O produtor substitui a submissao pendente quando uma mais nova fica pronta; o consumidor sempre pega a ultima completa disponivel. - -**Pro:** - -- implementa a politica ja aceita; -- evita fila infinita; -- desacopla VM e rasterizacao; -- aproxima desktop e hardware proprio por contrato de single/latest slot. - -**Con:** - -- exige `Send`/ownership/read-only real nos dados; -- precisa regra de shutdown, erro e sincronizacao; -- pode mascarar frames descartados sem telemetria adequada. - -**Maintainability:** - -Boa se o protocolo for pequeno e testavel. - -### Opcao C - Fila bounded com N submissions - -**Abordagem:** - -Usar canal bounded com 2 ou 3 submissions para suavizar picos e permitir que o render trabalhe atrasado. - -**Pro:** - -- simples em desktop; -- reduz bloqueios imediatos; -- permite medicao de atraso. - -**Con:** - -- contradiz parcialmente latest-wins se N crescer; -- aumenta memoria e latencia; -- pode apresentar frames velhos; -- menos adequado para handheld com memoria restrita. - -**Maintainability:** - -Media. Pode ser util como implementacao, mas precisa de regra forte para descarte. - -### Opcao D - Render thread como detalhe exclusivo do host desktop - -**Abordagem:** - -Mover rasterizacao/publicacao para thread do host, sem mudar contrato runtime/HAL. - -**Pro:** - -- melhora desktop sem mexer no console; -- menor impacto no runtime; -- facilita experimentos. - -**Con:** - -- pode criar divergencia entre host e alvo real; -- nao resolve contrato para hardware proprio; -- risco de esconder dependencia mutavel por baixo do host. - -**Maintainability:** - -Media a baixa como solucao canonica. Boa apenas para experimento ou profiling. - -## Sugestao / Recomendacao - -A recomendacao inicial e uma decisao em duas fases: - -1. Fechar agora as invariantes da Opcao A como contrato obrigatorio. -2. Planejar a Opcao B como primeiro modelo real de worker quando a base estiver pronta. - -O contrato desejado deve ser: - -```text -VM/runtime core - builds domain buffers - closes RenderSubmission snapshot - publishes latest complete submission to bounded handoff - -render worker/core - takes latest complete submission - consumes immutable packet/resource handles - rasterizes/publishes through RenderSurface - reports telemetry/backpressure -``` - -O worker nao deve receber `&mut Hardware`, `&mut Gfx`, `&mut VirtualMachineRuntime` ou ponte que permita voltar ao estado vivo da VM. - -## Perguntas Respondidas - -- [x] O handoff deve ser single-slot latest-wins, canal bounded N=2, ou outro protocolo? - - Resposta: single-slot latest-wins, sem bloquear VM/produtor. -- [x] Quais tipos dentro de `RenderSubmission` precisam ser `Send + Sync` ou owned? - - Resposta: submissions/packets cruzam a fronteira como dados owned pequenos; APIs read-only compartilhadas de bancos/cache precisam provar seguranca de compartilhamento antes do worker real. -- [x] Como assets, scene cache e glyph banks cruzam a fronteira sem copia pesada? - - Resposta: por IDs/handles estaveis e APIs read-only; bancos sao imutaveis por contrato, e o viewport cache pertence ao core logico. -- [x] Quem e dono do worker: `RenderManager`, host desktop, SystemOS service ou HAL? - - Resposta: `SystemOS`/lifecycle decide politica/AppMode; `RenderManager` coordena handoff, epoch, ownership e telemetry; host/HAL fornece capacidades e execucao concreta. -- [x] O render worker pode bloquear a VM ou apenas descartar/substituir submissions? - - Resposta: nao bloqueia a VM/produtor; descarta/substitui pending submissions conforme latest-wins. -- [x] Como medir frames produzidos, consumidos, descartados e apresentados? - - Resposta: counters e ultimos frame IDs separados para produced, replaced/dropped, consumed, presented, repeated, errors e stale epoch discards. -- [x] Como shutdown, crash, mode transition e troca de cart interagem com worker? - - Resposta: transicoes incrementam epoch/generation, invalidam pending/current obsoletos e impedem present de frames do owner anterior. -- [x] Qual parte deve ser especificada agora e qual deve esperar implementacao real? - - Resposta: especificar agora o contrato de packet fechado, handoff latest-wins, frame pacing, AppMode policy, epoch ownership e telemetry minima; implementacao concreta do worker/render backend fica para decision/plan posterior. - -## Grandes Problemas Esperados - -### Topico 1 - Game2DFramePacket como snapshot completo - -**Status:** direcao respondida; execucao pendente. - -**Pergunta de fechamento:** o consumidor de `RenderSubmissionPacket::Game2D` consegue compor o frame completo a partir do packet fechado, sem depender de caminho alternativo por estado vivo? - -Hoje `Hardware::publish_render_submission` ignora parte do `Game2DFramePacket` quando ha scene ativa, porque chama `frame_composer.render_frame(&mut self.gfx)` em vez de consumir o packet completo. Isso quebra a premissa de que o worker pode receber um snapshot fechado e rasterizar sem estado vivo. - -Antes de qualquer render worker, `Game2DFramePacket` precisa ser a fonte completa de composicao: scene/layers/sprites, `gfx2d` overlay e publicacao. - -**Direcao atual:** nao ha debate arquitetural relevante restante para este topico. `gfx2d` deve ser overlay apos a composicao Game 2D, e o consumidor de `RenderSubmissionPacket::Game2D` deve aplicar a mesma ordem em frames com scene ativa e sem scene ativa. O que falta e corrigir o caminho atual que contorna o packet quando ha scene ativa e adicionar teste de regressao cobrindo scene ativa + primitive overlay. - -### Topico 2 - Recursos pesados e fronteira read-only - -**Status:** direcao respondida. - -**Pergunta de fechamento:** como scene cache, glyph banks, memory banks, assets e recursos futuros cruzam a fronteira sem `&mut Hardware`, `&mut Gfx` ou `FrameComposer` vivo? - -`RenderSubmission` e owned, mas a composicao real ainda depende de `FrameComposer`, scene cache, resolver, glyph banks, memory banks e `Gfx`. Um worker nao pode receber `&mut Hardware` nem consultar `FrameComposer` vivo. Precisamos decidir como recursos pesados entram no snapshot: handles estaveis, `Arc` read-only, snapshots pequenos, ou acesso mediado. - -**O que falta discutir:** o ponto nao e copiar todos os recursos pesados para dentro do packet. Isso seria caro e provavelmente errado. O que falta e separar tres categorias: - -- estado de frame que deve estar no packet fechado: camera, scene binding, sprites, HUD, primitives, copy/update requests ja calculados; -- recursos residentes grandes que podem ser referenciados por handle/slot e lidos via `Arc`/read-only: glyph banks, scene banks, caches materializados; -- estado mutavel de preparacao que deve ficar no core logico antes do handoff: resolver update, cache refresh, asset installation, bank residency changes. - -**Direcao provavel:** o render worker deve receber um snapshot pequeno com comandos e handles estaveis, mais acesso read-only aos bancos/caches necessarios. A atualizacao de resolver/cache e a instalacao/troca de assets devem acontecer antes do handoff, no core logico ou em um servico proprietario, nunca durante rasterizacao no worker. - -**Perguntas respondidas:** - -- [x] O `Game2DFramePacket` deve carregar apenas `BoundScenePacket { bank_id }` ou tambem um snapshot/read handle do cache materializado? - - Resposta: IDs somente. Os bancos sao imutaveis por definicao; o packet nao deve capturar handles pesados por frame. -- [x] O cache de viewport pertence ao produtor logico, ao render worker, ou a um recurso compartilhado read-only apos refresh? - - Resposta: o viewport cache pertence ao core logico. O render worker deve acessa-lo apenas por uma API de leitura. -- [x] Uma troca de asset/bank pode invalidar recursos enquanto uma submission antiga ainda esta sendo rasterizada? - - Resposta: a submission aponta por ID para o recurso. Se o recurso for trocado, a submission ainda pode executar e o resultado pode ficar confuso/diferente. Disciplina de troca de assets e responsabilidade do dev ou framework. -- [x] O handoff deve capturar `Arc`/`Arc` por frame para garantir liveness, ou apenas IDs de slot com regra de nao substituir enquanto houver frame em voo? - - Resposta: somente IDs. Nao vamos garantir integridade do resultado por captura de recurso; disciplina de troca e responsabilidade do dev ou framework. -- [x] Quais tipos precisam provar `Send + Sync` antes de qualquer worker real? - - Esclarecimento: esta pergunta e tecnica. Em Rust, qualquer dado/API compartilhado entre threads precisa ser seguro para envio/leitura concorrente. Com a resposta "IDs somente" no packet, a exigencia sai do `Game2DFramePacket` pesado e recai principalmente sobre a API de leitura dos bancos/cache e sobre o protocolo de handoff. - - Resposta: essa prova fica como criterio tecnico de implementacao do worker real; a decisao arquitetural e que a fronteira compartilhada deve ser owned ou read-only, sem `&mut Hardware`, `&mut Gfx`, `FrameComposer` vivo ou acesso mutavel a VM. - -**Direcao respondida:** packet pequeno com IDs, bancos imutaveis por contrato, viewport cache pertencente ao core logico e acesso do worker por API read-only. O sistema nao deve tentar garantir integridade visual de submissions antigas contra troca disciplinarmente incorreta de assets/banks; essa disciplina pertence ao dev/framework. - -### Topico 3 - Protocolo de handoff latest-wins - -**Status:** direcao respondida. - -**Pergunta de fechamento:** qual protocolo substitui o `Option` local quando houver outro core/thread? - -O `latest_complete_submission` atual e apenas um `Option` dentro do runtime. Para outro core/thread, isso precisa virar protocolo: single-slot latest-wins, canal bounded, sequence numbers, descarte, acknowledgement e telemetria. - -**O que falta discutir:** este topico define o contrato de handoff, nao a politica de frame pacing completa. Precisamos escolher como uma submission sai do core logico e chega ao render worker: - -- single-slot latest-wins: produtor substitui a submission pendente por uma mais nova, e o worker consome a ultima disponivel; -- canal bounded N=1/2: produtor tenta enviar, e o protocolo decide se descarta antigo, descarta novo ou bloqueia; -- acknowledgement: worker informa qual `frame_id` consumiu/apresentou, ou o produtor apenas publica e esquece; -- ownership: a submission e movida para o handoff ou clonada do `RenderManager`; -- sequence numbers: `frame_id` basta ou precisamos de counters separados para produzido/consumido/apresentado; -- escopo por pipeline: Game pode usar handoff; Shell pode publicar sincrono. - -**Direcao provavel:** para o primeiro modelo real, preferir single-slot latest-wins, sem fila crescente e sem bloquear a VM. `RenderManager` deixa de ser apenas dono de `Option` e passa a publicar uma submission fechada para um handoff pequeno. O worker pega a ultima submission disponivel; se uma submission antiga for substituida antes do consumo, isso conta como drop/substituicao mensuravel. - -**Direcao aceita:** single-slot latest-wins, sem bloquear a VM/produtor. - -**Perguntas respondidas:** - -- [x] O slot guarda exatamente uma pending submission alem da currently rendering, ou apenas a ultima submission atomica disponivel? - - Resposta: o handoff guarda uma pending submission owned. Se o worker ja pegou uma submission, ela esta fora do slot e em consumo. O slot representa apenas a proxima/latest pending. -- [x] Quando o worker esta renderizando frame N e o core produz N+1 e N+2, N+1 deve ser descartado? - - Resposta: sim. `N+2` substitui `N+1` se `N+1` ainda nao foi retirado pelo worker. Isso e counted como replaced/dropped-before-consume. -- [x] O produtor precisa saber que uma submission foi consumida, ou so que foi substituida/publicada? - - Resposta: o produtor nao espera acknowledgement para continuar. O worker deve reportar consumed/presented como telemetria assíncrona, mas isso nao bloqueia nem participa da progressao da VM. - - Observacao: a preocupacao restante e a semantica do `tick`. Hoje VM e render estao dentro do mesmo tick sincrono. Com worker, o tick deve fechar/publicar a submission, mas nao esperar ACK de render. A relacao entre tick, present efetivo e telemetria fica para o Topico 4. -- [x] `frame_id` e suficiente para telemetria, ou precisamos de contadores separados de produced/dropped/consumed/presented? - - Resposta: `frame_id` e suficiente para ordenacao, mas nao para observabilidade. Precisamos de contadores separados: produced, replaced/dropped-before-consume, consumed, presented, render_errors/present_errors, e ultimos frame ids por etapa. -- [x] Shell UI fica fora do protocolo worker por default, ou usa o mesmo handoff quando configurado? - - Resposta: Shell UI fica fora por default e pode continuar sincrono/local. O mesmo protocolo pode ser reutilizado se uma configuracao futura quiser worker para Shell, mas a politica inicial e por pipeline/AppMode. - -**Direcao respondida:** o protocolo base e single-slot latest-wins com ownership moved para o handoff, pending slot substituivel, worker consumindo owned submission, VM/produtor nunca bloqueado, ack apenas para telemetria e politica por pipeline. - -Confirmacoes de 2026-06-04: - -- ownership moved para o handoff: aceito; -- produtor sem ACK bloqueante: aceito, com a preocupacao de definir a nova semantica de tick; -- contadores separados: seguir com essa direcao; -- Shell UI fora do worker por default: aceito. - -### Topico 4 - Frame pacing e backpressure - -**Status:** direcao respondida. - -**Pergunta de fechamento:** a VM bloqueia, descarta, substitui ou apenas mede atraso quando o render worker esta atrasado? - -Hoje tick, fechamento, raster e present acontecem no mesmo fluxo. Com worker separado, a VM pode produzir frames mais rapido que o render consome, ou o render pode apresentar em outra cadencia. Precisamos decidir se a VM bloqueia, descarta, substitui, ou apenas mede atraso. - -**Direcao em discussao:** se o render real for para worker/core, o `present` passa a ser responsabilidade do worker/render surface e deve mirar a cadencia de display, inicialmente 60Hz. O worker nao consome uma fila crescente; ele consome a latest submission disponivel pelo protocolo single-slot e apresenta em sua cadencia. - -O core logico/VM deixa de precisar reservar tempo de render dentro do mesmo tick sincrono, mas nao deve rodar livremente. O frame logico da VM e parte do modelo de tempo do jogo: animacoes, input, scripts e simulacao dependem dele. Portanto, a VM deve continuar paced por frame logico, mirando 1:1 com a cadencia do worker/display sempre que possivel. - -Idealmente, ha relacao 1:1: - -```text -1 logical VM frame --> 1 RenderSubmission --> 1 presented worker frame -``` - -Mas o contrato nao deve depender de sempre conseguir isso. A direcao revisada e: - -- o worker/render surface e dono do `present` em 60Hz; -- o worker apresenta o ultimo frame processado/disponivel; -- quando o worker aceita/pega uma nova submission para apresentacao, isso avanca o frame counter visivel; -- se a VM ainda nao disponibilizou uma nova submission, o worker mantem a ultima renderizacao e registra skip/no-new-frame; -- a VM nao roda frames logicos ilimitados a frente do display; ela so deve iniciar novo frame logico quando a cadencia permitir produzir o proximo frame esperado; -- se o worker ainda esta processando um frame anterior, a VM pode preparar/publicar o proximo frame logico dentro do limite 1:1, mas nao deve acumular multiplos frames logicos pendentes. - -Isso muda a leitura do latest-wins: ele continua util para substituir uma pending submission que ainda nao foi pega pelo worker, mas nao deve virar autorizacao para a VM rodar muito a frente do display. O objetivo e desacoplar custo de raster/present, nao desacoplar o tempo logico do jogo da cadencia de frame. - -Se o worker nao rasterizar dentro do budget de 60Hz, o sistema deve contar atraso/drop/skip em telemetria. A VM nao deve bloquear esperando ACK de render, mas deve respeitar o pace de frame logico definido pelo display/worker. - -Refinamento importante: - -A VM nao espera ACK de render/present de uma submission especifica, mas deve esperar a autorizacao de pace derivada do frame counter/display. Ou seja, o render worker nao confirma "terminei seu frame N, pode seguir"; em vez disso, o sistema avanca uma cadencia de frame e a VM so inicia o proximo logical frame quando essa cadencia permite. Isso evita que a VM rode livremente sem transformar present em bloqueio sincrono. - -### Corner cases de frame pacing - -1. Worker em 60Hz, VM pronta antes do proximo refresh. - - A VM deve ficar aguardando o proximo frame pace para iniciar novo logical frame. - - Nao deve produzir multiplas submissions futuras. - -2. Worker apresenta refresh sem nova submission. - - O worker repete/mantem a ultima renderizacao. - - O sistema registra skip/no-new-frame. - - O frame counter visivel pode avancar, mas a VM nao deve simular um novo logical frame retroativamente. - -3. VM produz submission, worker ainda esta rasterizando frame anterior. - - A submission nova fica no single-slot pending. - - Se outra submission fosse produzida antes do worker pegar a pending, ela substituiria a pending; porem o pace da VM deve impedir esse acúmulo na pratica. - -4. Worker pega pending submission para rasterizar. - - A pending sai do slot e passa a pertencer ao worker. - - A VM nao espera ACK dessa submission, mas so inicia novo logical frame quando o pace permitir. - -5. Worker perde prazo de 16.67ms. - - O sistema registra atraso/render-overrun. - - A VM nao deve tentar compensar produzindo frames extras. - - A proxima permissao de logical frame deve seguir a politica de pace, nao backlog. - -6. VM demora mais que a janela esperada de logical frame. - - O runtime nao deve encerrar o frame por budget como mecanismo normal. - - `FRAME_SYNC` continua sendo o fim canonico do logical frame. - - O sistema registra logical-frame-overrun para certificacao/watchdog/diagnostico. - - O worker continua apresentando a ultima renderizacao. - - Quando a VM finalmente publica a submission, ela representa o proximo logical frame sequencial, nao um frame pulado. - -7. Mode transition Game/Shell enquanto ha pending ou currently-rendering submission. - - A submission antiga nao deve ser apresentada depois que o modo mudou, a menos que a politica de transition permita explicitamente. - - O handoff precisa carregar generation/mode epoch ou equivalente para invalidar frames obsoletos. - -8. Cartridge/app troca enquanto worker tem frame antigo. - - Submissions de app/cart anterior devem ser descartadas por generation/epoch. - - O worker deve limpar ou substituir a surface conforme politica de lifecycle. - -9. Crash/panic durante logical frame antes de publicar submission. - - O worker segue apresentando ultimo frame valido ate receber crash/shell submission. - - O crash path deve poder invalidar pending game frame. - -10. Crash/panic no render worker. - - Nao pode corromper determinismo da VM. - - Deve virar telemetria/erro de render e possivelmente fallback para sync/local render ou crash surface. - -11. Input sampling. - - Input deve ser amostrado no inicio do logical frame autorizado pelo pace. - - Se o worker repetir frame por falta de submission nova, nao deve haver novo input sampling/logical update correspondente. - -12. Frame counter semantics. - - Precisamos separar pelo menos logical_frame_id, produced_submission_frame_id e presented_frame_id. - - O frame counter que autoriza a VM deve representar cadencia logica/display, nao ACK de uma submission especifica. - -### Avaliacao de desenho - -O desenho fica mais complexo se o render worker tambem virar a autoridade do frame counter que libera a VM. Isso mistura duas responsabilidades: - -- cadencia logica do sistema; -- consumo/rasterizacao/publicacao de submissions. - -Uma alternativa mais limpa e introduzir uma autoridade de pace separada, por exemplo `FrameClock` ou scheduler de frame, que emite ticks de 60Hz. A VM e o render worker observam essa cadencia, mas nenhum dos dois e dono exclusivo dela. - -Modelo preferido para reduzir complexidade: - -```text -FrameClock / frame scheduler - emits 60Hz frame ticks - -VM/core logico - runs at most one logical frame per frame tick - samples input on authorized logical frame - publishes at most one latest submission - -render worker/surface - presents on display cadence - consumes latest available submission - repeats last rendered frame if no new submission exists -``` - -Assim, a VM nao espera ACK do worker e tambem nao roda livremente. Ela espera o frame clock. O worker nao precisa "desbloquear" a VM; ele apenas consome e apresenta. Isso preserva 1:1 como alvo sem acoplar progresso logico a sucesso/falha de rasterizacao. - -Conclusao provisoria: preferir `FrameClock`/scheduler como autoridade de cadencia, `RenderManager` como produtor de submissions, e render worker como consumidor/presenter. Evitar que o worker seja dono do frame counter canonico. - -Refinamento sobre Shell/App lifecycle: - -O `FrameClock`/scheduler nao deve impor um game loop ao Shell. O loop de um game e diferente do lifecycle de um app UI, e em Prometeu o game loop e entregue no hardware/runtime como parte do modelo de console. Shell/SystemOS deve ser tratado como lifecycle/event-driven: input, janelas, foco, app lifecycle e invalidacao visual determinam quando ha trabalho logico ou render novo. - -Portanto, `FrameClock` deve autorizar cadencia de logical frame para pipelines de game, mas Shell UI nao deve necessariamente consumir um frame logico de VM a cada 60Hz. Para Shell: - -```text -SystemOS/Shell lifecycle events --> input/window/app updates when needed --> build ShellUiFramePacket on invalidation/event --> present/repeat last surface as needed -``` - -Isso reforca que `FrameScheduler` nao e "scheduler da VM". Ele e uma fonte de cadencia para workloads que precisam de frame pacing, especialmente Game2D/Game3D. Shell/App lifecycle pode usar outro modelo, com render por invalidacao ou por eventos, ainda que o host/display continue apresentando em 60Hz. - -Resposta sobre budget e frames lentos: - -`FRAME_SYNC` e o fim canonico do logical frame. O budget antigo era um mecanismo grosseiro para pausar a VM e permitir que o tick continuasse; nesse desenho ele nao deve ser usado como forma normal de encerrar ou cortar um frame logico. - -Budget passa a existir para certificacao, watchdog, diagnostico e deteccao de overrun. Se um logical frame demora mais que aproximadamente 16.67ms, nao pulamos logical frames da VM para alcançar o display. Frames logicos devem ser sequenciais. O resultado visivel e stutter/repeticao do ultimo frame pelo worker, nao time-skip semantico. - -```text -VM logical frame N demora demais --> worker repete ultimo frame disponivel --> runtime registra overrun --> VM continua ate FRAME_SYNC/trap/halt/watchdog fatal --> submission publicada e N, nao N+k -``` - -**Perguntas respondidas:** - -- [x] O worker deve repetir o ultimo frame quando nao houver submission nova no refresh de 60Hz? - - Resposta: sim. O worker mantem a ultima renderizacao e registra skip/no-new-frame. -- [x] A VM deve continuar limitada a um logical frame por tick de sistema, ou pode produzir multiplas submissions entre presents? - - Resposta: a VM nao deve produzir multiplos frames logicos a frente do display. O alvo e 1 logical frame por frame worker/display, com no maximo uma pending submission pelo protocolo latest-wins. -- [x] `FRAME_SYNC` continua sendo o limite canonico de logical frame mesmo sem present sincrono? - - Resposta: sim. `FRAME_SYNC` continua delimitando logical frame da VM. O present deixa de ser sincrono, mas o frame logico continua paced. -- [x] Qual budget permanece na VM: ciclos/instrucoes por logical frame, tempo real, ou ambos? - - Resposta: `FRAME_SYNC` encerra o logical frame. Budget nao e mecanismo normal de tick/frame advance. Budget/ciclos/tempo real ficam para certificacao, watchdog, diagnostico e overrun policy. -- [x] Como detectar game rodando rapido demais se o render worker esta desacoplado? - - Resposta: o runtime nao permite que a VM rode livremente a frente do frame counter/worker. A telemetria deve contar produced, accepted/presented e skips para detectar desalinhamento. - -### Topico 5 - Politica por pipeline/AppMode - -**Status:** direcao respondida; detalhamento de lifecycle movido para `DSC-0041`. - -**Pergunta de fechamento:** quais pipelines devem poder usar worker e quais podem continuar sincronos/localmente? - -Shell UI pode continuar sincrona/local. O contrato comum deve ser packet fechado, mas a politica de execucao deve variar por pipeline. Forcar Shell UI para render worker pode aumentar complexidade sem ganho. - -**O que precisamos acertar:** este topico define a matriz de politica por `AppMode`/pipeline. O sistema hoje tem `AppMode::Game` e `AppMode::Shell`, mas esses modos nao devem receber a mesma semantica de loop/render. - -Direcao ja encaminhada: - -- `Game2D` e futuro `Game3D` sao workloads frame-paced. Devem poder usar `FrameScheduler` e render worker. -- `ShellUi` e Shell/SystemOS sao lifecycle/event-driven. Devem poder continuar sincronos/localmente e renderizar por invalidacao/evento. -- O contrato comum continua sendo `RenderSubmission` fechado. -- A politica de execucao do consumidor pode variar por pipeline/AppMode. - -**Perguntas respondidas:** - -- [x] `AppMode::Game` sempre usa `FrameScheduler`, mesmo se o game nao tiver render pesado? - - Resposta: sim. Game e frame-paced por definicao. Futuramente `AppMode::Game` pode virar algo mais explicito como `AppMode::Game2D`, mas cada AppMode deve ter uma politica de render explicita. -- [x] `AppMode::Game` deve usar render worker por default, ou isso deve ser capability/configuracao de host? - - Resposta: Game usa render worker por default quando o host/runtime suporta, com fallback sincrono/local. -- [x] `AppMode::Shell` deve ficar explicitamente fora do `FrameScheduler` logico de game? - - Resposta: sim. Shell nao herda game loop. -- [x] Shell UI deve publicar sincrono/local por default, com worker apenas opt-in? - - Resposta: sim. Shell UI e sincrono/local por default; worker so como opt-in futuro. -- [x] Apps VM-backed dentro do Shell seguem lifecycle de Shell ou podem declarar workload frame-paced proprio? - - Resposta: seguem lifecycle de Shell, sem direito a declarar workload frame-paced proprio. -- [x] Firmware screens como splash/crash seguem politica Shell/local ou politica propria? - - Resposta: seguem politica Shell/local. -- [x] A escolha de politica fica no `RenderManager`, no SystemOS/lifecycle, ou em uma camada de host/runtime configuration? - - Resposta: SystemOS/lifecycle decide o perfil/AppMode; `RenderManager` executa a politica; host/runtime fornece capacidades e fallback. - -**Direcao respondida:** cada AppMode deve ter politica explicita de render. Game atual e frame-paced e pode usar worker por default quando disponivel. Shell e lifecycle/event-driven e sincrono/local por default. Apps VM-backed dentro do Shell seguem lifecycle de Shell, sem opt-in para game pacing proprio. - -### Topico 6 - Lifecycle, transitions e ownership da surface - -**Status:** direcao respondida; detalhamento de lifecycle movido para `DSC-0041`. - -**Pergunta de fechamento:** quem possui a surface/submission durante troca Game/Shell, crash, splash, troca de cart e shutdown? - -Troca Game/Shell, crash screen, splash, troca de cart e shutdown precisam saber quem possui a surface e qual submission ainda pode ser publicada. O worker nao pode ficar publicando frame velho depois que o modo ou cartridge mudou. - -**O que precisamos acertar:** quando existe worker/render assíncrono, podem existir submissions em tres estados: - -- pending no single-slot; -- currently-rendering no worker; -- ultimo frame apresentado/mantido na surface. - -Lifecycle precisa dizer o que acontece com cada um quando o sistema muda de dono visual. - -**Direcao provavel:** toda submission deve carregar um contexto de ownership alem de `frame_id` e `app_mode`, por exemplo `render_epoch`/`generation`, `app_id` ou `surface_owner`. O `RenderManager`/SystemOS incrementa essa generation quando troca app, cart, modo ou crash state. O worker deve descartar pending/current frames cujo ownership nao combine com o epoch ativo antes de apresentar. - -**Perguntas respondidas para o escopo desta agenda:** - -- [x] Troca Game -> Shell invalida imediatamente pending/current Game submissions? - - Resposta: sim. A troca de foreground visual owner incrementa epoch/generation e torna obsoletas as submissions do owner anterior. -- [x] Troca Shell -> Game deve limpar surface ou pode manter ultimo Shell frame ate primeiro Game frame? - - Resposta: pode manter a ultima surface apresentada ate a primeira submission valida do novo owner, desde que nenhum frame antigo seja apresentado novamente como nova submission. Limpar e uma politica visual opcional. -- [x] Crash screen invalida pending/current Game frame antes de publicar crash Shell/local frame? - - Resposta: sim. Crash troca o owner visual para a tela de erro/sistema e deve invalidar frames pendentes/correntes do Game. -- [x] Splash/crash/hub usam uma surface compartilhada ou ownership proprio? - - Resposta: podem usar a mesma surface fisica, mas precisam de ownership logico proprio via epoch/generation/surface owner. -- [x] Troca de cartridge/app incrementa `render_epoch` mesmo se `AppMode` continuar igual? - - Resposta: sim. `AppMode` nao e identidade suficiente; troca de app/cart precisa invalidar submissions antigas. -- [x] Worker pode terminar raster de um frame obsoleto, mas deve checar epoch antes de present? - - Resposta: sim. O worker pode desperdiçar trabalho ja iniciado, mas nao pode apresentar se o epoch/owner nao for mais atual. -- [x] Quem incrementa epoch: SystemOS/lifecycle, `RenderManager`, host, ou todos via API central? - - Resposta: `SystemOS`/lifecycle decide a transicao semantica; `RenderManager` deve oferecer a API central que incrementa/aplica epoch. Host nao decide ownership. -- [x] Shutdown deve drenar worker, cancelar pending/current, ou apenas parar present? - - Resposta: shutdown deve parar present, descartar pending, invalidar current por epoch/stop token e encerrar/joinar o worker de forma bounded. Nao deve drenar frames obsoletos para a tela. - -**Levantamento de material existente:** - -- `DSC-0031` / `LSN-0040` ja estabeleceu `AppMode` como discriminador de perfil. Game e Shell/System usam a mesma VM/transporte internamente, mas nao compartilham o mesmo perfil autoral. Game segue pipeline de jogo; Shell/System segue pipeline Runtime/Hub. -- `DSC-0032` / `LSN-0041` ja estabeleceu `SystemOS` como autoridade semantica de lifecycle para task/process. Firmware nao deve coordenar manualmente task/process fora dessa fronteira. -- `DSC-0035` / `LSN-0044` ja estabeleceu que Shell liveness depende da janela focada pertencer ao proprio task; perder a janela elegivel fecha o Shell task via lifecycle e volta ao Hub. -- `DSC-0036` / `LSN-0045` ja estabeleceu o fluxo visual/lifecycle `Hub/Home -> Shell app -> task-owned window -> close -> lifecycle close -> Hub/Home`. -- `DSC-0038` / `LSN-0047` ja estabeleceu que `RenderManager` coordena active app mode, submission closure, transitions, capabilities e publication flow, mas sem virar renderer. - -**Estado atual de implementacao:** - -- Firmware possui estados explicitos (`GameRunning`, `ShellRunning`, `AppCrashes`, splash/hub/load) e `change_state` executa `on_exit`/`on_enter` imediatamente para evitar frames vazios. -- `LoadCartridgeStep` roteia `AppMode::Shell` para task/window Shell e `ShellRunning`; caso contrario cria task de game e entra em `GameRunning`. -- `GameRunningStep` exige task foreground e executa `ctx.os.vm().tick(...)`. -- `ShellRunningStep` exige task foreground e janela focada pertencente ao task; caso contrario fecha pelo lifecycle e volta ao Hub. -- `RenderManager` atual tem `RenderTransitionState::Pending { from, to }`, mas isso e placeholder/no-op: nao ha epoch/generation, invalidacao de pending/current frames, nem ownership de surface. - -**Gap para este topico:** as politicas de lifecycle Game/Shell existem, mas ainda nao foram propagadas para ownership visual assíncrono. Para `DSC-0040`, a direcao e suficiente: toda transicao de foreground visual owner passa por uma API central do `RenderManager`, incrementa epoch/generation, invalida pending/current do owner anterior e impede present de frames obsoletos. - -### Caso principal - Home durante Game e retorno ao Game - -Cenario esperado: - -```text -GameRunning --> usuario aperta Home --> Game e suspenso/pausado como task foreground --> Hub/Home assume Shell UI --> usuario navega/checa algo no Shell --> usuario retorna ao Game --> Game volta como foreground -``` - -Mesmo sem transicoes suaves, esse ciclo precisa funcionar. O estado atual cobre partes do modelo, mas nao o ciclo completo: - -- ja existe `GameRunningStep` para game fullscreen foreground; -- ja existe `HubHomeStep` e `ShellRunningStep` para Hub/Shell lifecycle; -- ja existe lifecycle authority em `SystemOS`; -- ja existe Shell task/window liveness; -- ainda nao ha evidencia de um fluxo Home durante `GameRunning` que suspenda o game, entre no Hub, e depois retorne ao mesmo game; -- o `RenderManager` so possui `RenderTransitionState::Pending { from, to }` como placeholder; nao ha ownership/epoch para invalidar frames de Game enquanto o Hub assume a surface. - -Politica provavel para esse caso: - -- Home em Game nao fecha o game; suspende/pausa o game task/process. -- Ao entrar no Hub, qualquer pending/current Game submission deve ser invalidada por epoch/generation antes de Shell/Hub assumir a surface. -- Hub/Shell renderiza local/sincrono por politica Shell. -- Retornar ao Game incrementa epoch novamente e retoma o game task/process. -- O primeiro frame de retorno ao Game deve vir de uma nova Game submission; ate la a surface pode manter ultimo Hub frame ou limpar conforme politica visual. -- Transicoes suaves ficam fora do escopo; o requisito minimo e ownership correto e ausencia de frame obsoleto apresentado apos troca de dono. - -Questao de escopo levantada: - -No ciclo `Game -> Shell -> Game`, pode existir um game pausado e um app Shell VM-backed aberto ao mesmo tempo. Isso cruza render ownership com lifecycle/process policy. - -Direcao provisoria para esta agenda: - -- somente um Game foreground/pausado por vez; -- somente um Shell foreground aberto por vez no v1; -- background execution fica fora deste contrato; -- Shell VM-backed durante Game pausado deve seguir lifecycle de Shell e nao ganha politica frame-paced propria; -- a VM/game pausado nao deve continuar executando frames logicos enquanto o Shell VM-backed esta foreground; -- a existencia simultanea de um Game pausado e um Shell VM-backed foreground precisa ser tratada como politica de SystemOS/lifecycle, nao como detalhe do render worker. - -Essa pergunta foi separada na `DSC-0041`. Para `DSC-0040`, o ponto minimo esta respondido: render ownership deve acompanhar o foreground visual owner e invalidar submissions do owner anterior por epoch/generation. - -### Topico 7 - Erros, drops e telemetria de render - -**Status:** direcao respondida. - -**Pergunta de fechamento:** quais eventos minimos o sistema registra para render produzido, consumido, descartado, atrasado, apresentado e com erro? - -Quando rasterizacao sai do fluxo da VM, panic/erro de render, frame drop, atraso, consumo e present precisam ser reportados sem quebrar determinismo da VM. Sem telemetria minima, latest-wins pode esconder perda de frames. - -**Direcao aceita:** render assíncrono e best-effort observavel, nao handshake semantico com a VM. A VM produz frames sequenciais no ritmo do `FrameScheduler`; o worker consome/apresenta o mais recente possivel; drops, repeats, erros e descartes por epoch/stale ownership aparecem em telemetry, mas nao alteram diretamente a semantica do programa dentro da VM. - -**Contrato minimo:** - -- a VM nao observa erro/drop de render como retorno sincrono nem como semantica de programa; -- render telemetry e preocupacao de host/runtime/system, debugger, profiler e certificacao; -- erro de raster descarta a submission afetada, registra telemetry e preserva o ultimo frame valido/surface atual quando possivel; -- erro de present registra telemetry e pode escalar para politica de host/system fault se for recorrente; -- panic no worker deve virar falha controlada de worker/backend, nao panic propagado para a VM; -- o programa dentro da VM nao deve depender de confirmacao de present para avancar logica. - -**Counters minimos:** - -- `produced_submissions`; -- `replaced_before_consume`; -- `consumed_submissions`; -- `presented_frames`; -- `repeated_presents`; -- `render_errors`; -- `present_errors`; -- `stale_epoch_discards`; -- `shutdown_discards`. - -**IDs/estado minimo:** - -- `last_produced_frame_id`; -- `last_consumed_frame_id`; -- `last_presented_frame_id`; -- `last_dropped_frame_id`; -- `last_error_frame_id`; -- `active_render_epoch`. - -**Eventos minimos:** - -- submission substituida no single-slot antes de consumo; -- worker consumiu uma submission; -- worker descartou submission por epoch/owner obsoleto; -- worker apresentou frame; -- worker repetiu ultimo frame por ausencia de submission nova; -- erro de raster; -- erro de present; -- shutdown/cancel descartou pending/current. - -## Fechamento dos Topicos - -Esta agenda pode ser considerada madura para decisao quando os sete topicos acima tiverem uma resposta de direcao. As respostas nao precisam implementar render worker agora, mas precisam ser concretas o suficiente para produzir uma decision sem reabrir a arquitetura base durante o plan. - -## Criterio para Encerrar - -Esta agenda pode virar decisao quando houver consenso sobre: - -- protocolo de handoff VM/render; -- ownership de submissions e recursos compartilhados; -- politica de backpressure; -- fronteira entre runtime contract e implementacao host/hardware; -- telemetria minima; -- o que fica fora de escopo antes do primeiro worker real. - -## Discussion - -Aberta em 2026-06-04 apos verificar que `DSC-0038` preparou snapshots fechados e latest-wins, mas nao decidiu thread/core separado. - -Atualizacao de 2026-06-04: - -Antes de discutir worker/thread de render, precisamos fechar uma pre-condicao de composicao do pacote atual. A pergunta operacional foi: comandos de primitivas em `gfx2d` sao desenhados apos qualquer blit/raster dos layers? - -O estado atual e misto: - -- No caminho direto `Gfx::render_game2d_frame_packet`, `render_no_scene_frame()` roda antes e depois `packet.gfx2d` e aplicado. Nesse caso, primitivas ficam depois da composicao base. -- No caminho real de `Hardware::publish_render_submission`, quando `frame_composer.active_scene_id().is_some()`, o codigo ignora o `Game2DFramePacket` recebido e chama `frame_composer.render_frame(&mut self.gfx)`. -- `FrameComposer::render_frame` atualiza cache/resolver e chama `gfx.render_scene_from_cache(...)`, que faz o blit/raster dos layers, mas nao aplica `packet.gfx2d`. - -Conclusao provisoria: para frames com scene ativa, ainda nao ha garantia de que `gfx2d` seja desenhado apos os layer blits; na pratica, esses comandos podem ser descartados nesse branch. Isso deve ser resolvido antes de transformar `RenderSubmission` em fronteira de handoff para outro worker/core, porque o worker precisa consumir o pacote fechado completo, nao reconstruir comportamento a partir de estado vivo do `FrameComposer`. - -Resposta de direcao em 2026-06-04: - -As primitivas `gfx2d` devem funcionar como overlay do frame Game 2D. Elas precisam ser desenhadas apos a composicao canonical de scene/layers/sprites tanto em frames sem scene ativa quanto em frames com scene ativa. O comportamento esperado e: - -```text -Game2D composer/layers/sprites --> gfx2d primitives overlay --> present/publication -``` - -Isso implica que o branch com scene ativa em `Hardware::publish_render_submission` nao deve contornar o `Game2DFramePacket` nem perder `packet.gfx2d`. A solucao futura deve fazer o consumidor de `RenderSubmissionPacket::Game2D` aplicar a mesma ordem de composicao nos dois caminhos. - -Essa regra tambem reforca a fronteira necessaria para render worker/thread: o pacote fechado deve conter tudo que o consumidor precisa para compor o frame visivel. O worker nao deve depender de chamar um caminho alternativo baseado em estado vivo do `FrameComposer` que ignora parte do packet. - -Atualizacao sobre Shell UI em 2026-06-04: - -`ShellUi` e `Game2D` sao separados no contrato e no roteamento, mas compartilham os mesmos helpers finais de desenho no `Gfx`. `ShellUiFramePacket` contem comandos `gfxui`, enquanto `Game2DFramePacket` contem `composer` + `gfx2d`. No consumidor local, `apply_gfxui_command` e `apply_gfx2d_command` chamam os mesmos primitives finais (`clear`, `fill_rect`, `draw_line`, `draw_text`, etc.). - -Mover o render real para outro core/thread nao deve atrapalhar Shell UI se a fronteira for respeitada: Shell update/layout/input continuam no core logico, produzem um `ShellUiFramePacket` fechado, e o render worker consome apenas esse snapshot imutavel. - -O risco especifico do Shell UI nao e rasterizar em outro core. O risco e deixar o worker depender de estado vivo de Shell/SystemOS, janelas, ponteiros ou input enquanto desenha. Hoje o Hub monta o packet lendo estado de OS/janelas/input e publica imediatamente. Num modelo paralelo, essa montagem deve continuar antes do handoff; o worker deve receber so comandos fechados e recursos estaveis. - -Portanto, para Shell UI, a regra de handoff deve ser: - -```text -Shell/SystemOS update + input sampling --> build ShellUiFramePacket from current UI state --> hand off immutable ShellUi submission --> render worker consumes commands only -``` - -Isso preserva a UI sobre primitivas sem exigir que o render worker entenda window manager, foco, input ou lifecycle. - -Direcao refinada em 2026-06-04: - -Render paralelo faz mais sentido como capacidade para workloads de game 2D/3D do que como obrigacao universal do runtime. Um app UI/Shell pode nao se beneficiar de separar rasterizacao em outro core, porque seu custo principal tende a estar em estado de UI, layout, input, foco, janelas e lifecycle, nao em um pipeline grafico pesado. - -Portanto, a decisao futura nao deve assumir que todo `RenderSubmission` passa obrigatoriamente por um worker dedicado. A politica deve poder variar por `AppMode`/pipeline: - -- `Game2D`/futuro `Game3D` podem usar handoff para render worker/core quando houver ganho real de frame time ou isolamento. -- `ShellUi` pode continuar em consumo sincrono/local se isso for mais simples, deterministico e barato. -- O contrato comum deve continuar sendo o packet fechado; a politica de execucao do consumidor pode ser diferente por pipeline. - -Essa distincao evita otimizar a Shell UI como se fosse um renderer de jogo. Para UI, o valor principal de `ShellUiFramePacket` e manter a fronteira limpa e testavel; paralelismo deve ser opcional. - -## Resolution - -Direcao da agenda respondida. Os sete topicos tem recomendacao suficiente para virar decision: - -- `Game2DFramePacket` deve ser snapshot completo, com `gfx2d` como overlay apos composicao de scene/layers/sprites; -- recursos pesados cruzam a fronteira por IDs/handles estaveis e APIs read-only, nao por handles mutaveis vivos; -- handoff base e single-slot latest-wins, sem bloquear a VM/produtor; -- frame pacing fica sob `FrameClock`/`FrameScheduler`, nao sob ACK do render worker; -- politica de render varia por `AppMode`/pipeline: Game frame-paced; Shell lifecycle/event-driven e local/sincrono por default; -- transicoes de foreground visual owner usam epoch/generation para invalidar submissions obsoletas; -- telemetria minima torna drops, repeats, erros e stale discards observaveis sem virar semantica da VM. - -Detalhes de foreground stack, Game pausado e coexistencia com Shell VM-backed foram separados na `DSC-0041`. - -## Next Step - -Transformar esta agenda em decision antes de qualquer spec ou codigo de render worker. diff --git a/discussion/workflow/agendas/AGD-0042-real-render-worker-establishment.md b/discussion/workflow/agendas/AGD-0042-real-render-worker-establishment.md new file mode 100644 index 00000000..944cbf7f --- /dev/null +++ b/discussion/workflow/agendas/AGD-0042-real-render-worker-establishment.md @@ -0,0 +1,172 @@ +--- +id: AGD-0042 +ticket: real-render-worker-establishment +title: Real Render Worker Establishment +status: open +created: 2026-06-06 +resolved: +decision: +tags: [runtime, renderer, worker, concurrency, host, hal, architecture] +--- + +## Contexto + +`DEC-0031` fechou a arquitetura base da fronteira VM/render: submissions fechadas, handoff single-slot latest-wins, pacing por `FrameScheduler`, politica explicita por `AppMode`, ownership/epoch e telemetria. + +A implementacao atual ja tem a base local/sincrona e um `LocalRenderWorker` cooperativo por capability flag. Isso valida o roteamento e a politica, mas ainda nao estabelece um worker real em outra thread/core. O proximo passo arquitetural e transformar esse prototipo em uma unidade de execucao real sem reabrir a decisao base. + +Esta agenda existe para discutir somente o estabelecimento do worker real. Ela nao deve redecidir `gfx2d` como overlay, o packet boundary, AppMode policy, ou latest-wins; esses pontos pertencem a `DEC-0031`. + +## Problema + +Um worker real precisa cruzar fronteiras que o prototipo local ainda evita: + +- ownership e lifetime de recursos compartilhados entre VM/logical core e render worker; +- como o worker recebe, consome, rasteriza e apresenta submissions sem `&mut Hardware`, `&mut Gfx`, `FrameComposer` vivo ou estado mutavel da VM; +- como o viewport cache/read model fica seguro e barato; +- como shutdown, stop token, current work e stale owner funcionam quando ha trabalho realmente em voo; +- onde fica o present loop e se ele roda a 60Hz independentemente da VM; +- como erros de render/present deixam de ser apenas panic containment e viram resultado tipado; +- como host desktop, hardware proprio, outro core, DMA ou coprocessador mapeiam para o mesmo contrato. + +## Pontos Criticos + +### 1. Ownership do worker + +Precisamos decidir se o worker real pertence ao runtime, ao host, a HAL, ou a uma camada hibrida. `DEC-0031` diz que host/HAL fornece capability concreta, mas `RenderManager` coordena contrato. Falta definir o boundary operacional. + +### 2. Surface e backend renderizavel + +O worker nao pode receber `&mut Hardware` ou `&mut Gfx` da VM thread. Precisamos de um objeto/capability renderizavel que seja owned pelo worker ou mediado por host. + +### 3. Recursos read-only e viewport cache + +Glyph/scene banks ja tem boundary read-only inicial. O viewport cache ainda precisa de um modelo concreto: snapshot, `Arc` read-only, double-buffer cache, cache materializado por frame, ou service proprietario. + +### 4. Current work e cancelamento + +O `RenderManager` ja descarta pending e rejeita stale epoch antes de present. Com worker real, uma submission pode estar em rasterizacao. Precisamos definir stop token, epoch check antes de present, bounded join e descarte de current work. + +### 5. Present loop e frame repeat + +Hoje repeated present e telemetria/hook. Um worker real provavelmente precisa de loop de present/display cadence. Falta decidir se o worker apresenta a 60Hz, se o host faz scanout/repeat, e como isso conversa com `FrameScheduler`. + +### 6. Erros tipados + +`RenderSurface` hoje e infalivel e panic containment vira `PresentFailed`. Para worker real, render/present deveria retornar erro tipado, distinguindo falha recuperavel, backend lost, surface lost, panic, shutdown e stale discard. + +### 7. Testabilidade + +Precisamos de testes que provem thread boundary, latest-wins sob atraso real, shutdown com current work em voo, stale epoch durante rasterizacao, repeat do ultimo frame e ausencia de bloqueio da VM. + +## Opcoes + +### Opcao A - Worker real dentro do runtime + +**Abordagem:** `RenderManager` ou um novo `RenderWorkerRuntime` cria e gerencia uma thread de render, canais/slots, stop token e lifecycle. + +**Pros:** +- contrato fica perto de `RenderManager`; +- facil testar com runtime puro; +- menos dependencia de host desktop. + +**Contras:** +- risco de runtime assumir detalhes de surface/window que pertencem ao host; +- pode ficar ruim para hardware proprio, DMA ou coprocessador; +- exige abstrair muito bem o backend renderizavel. + +**Manutenibilidade:** boa se o backend for uma trait pequena e owned pelo worker; ruim se vazar `Hardware`/`Gfx` vivo. + +### Opcao B - Worker real como responsabilidade do host + +**Abordagem:** runtime fecha submissions e handoff; host implementa thread/core/present loop usando uma capability HAL/host. + +**Pros:** +- encaixa melhor desktop windowing, swapchain, surface lost e display cadence; +- facilita mapeamento para plataformas diferentes; +- runtime continua sem detalhe de thread/window. + +**Contras:** +- testes de contrato podem ficar mais integrados e menos unitarios; +- risco de cada host reinterpretar o contrato; +- `RenderManager` precisa expor hooks suficientes sem virar passivo demais. + +**Manutenibilidade:** boa para plataformas diversas, desde que exista uma suite de contrato obrigatoria. + +### Opcao C - Worker como HAL/backend owned, coordenado pelo runtime + +**Abordagem:** runtime possui `RenderManager` e policy; HAL/host fornece um `RenderBackend`/`RenderWorkerBackend` owned e thread-safe. Um `RenderWorkerController` conecta handoff, stop token, epoch e telemetry. + +**Pros:** +- separa contrato runtime de implementacao de host; +- permite worker thread no desktop e outro core/DMA em hardware; +- cria ponto unico para testes de contrato; +- evita passar `&mut Hardware` ou `&mut Gfx` pela fronteira. + +**Contras:** +- mais desenho inicial; +- exige definir traits novas com cuidado; +- pode ser excessivo se o primeiro worker real for apenas desktop. + +**Manutenibilidade:** melhor direcao se quisermos preservar portabilidade e nao acoplar runtime a winit/surface. + +### Opcao D - Adiar worker real e manter prototipo local + +**Abordagem:** nao implementar worker real agora; apenas fortalecer testes/spec e manter `LocalRenderWorker`. + +**Pros:** +- menor risco imediato; +- evita mexer em host/surface antes da necessidade real. + +**Contras:** +- nao valida thread/core real; +- deixa lacunas de Send/Sync, shutdown current work e present cadence; +- pode mascarar problemas de ownership ate tarde. + +**Manutenibilidade:** aceitavel por curto prazo, fraca se a demanda agora e paralelismo real. + +## Sugestao / Recomendacao + +Recomendo seguir pela **Opcao C - Worker como HAL/backend owned, coordenado pelo runtime**. + +O desenho deve manter `RenderManager` como dono de policy, epoch, handoff e telemetry, mas introduzir uma camada operacional para worker real com: + +- backend/render surface owned pelo worker ou pelo host; +- handoff single-slot sem bloquear VM; +- stop token e bounded shutdown; +- check de epoch/owner antes de present; +- erro tipado para render/present; +- present cadence explicita, com repeat do ultimo frame quando nao houver submission nova; +- testes de contrato que rodem sem janela nativa sempre que possivel. + +Essa recomendacao preserva `DEC-0031` e evita transformar o runtime puro em dono de detalhes de window/swapchain. + +## Perguntas em Aberto + +- [ ] O primeiro worker real deve nascer no host desktop/winit ou em uma abstracao runtime/HAL testavel sem janela? +- [ ] Qual trait substitui `RenderSurface::consume_submission(&RenderSubmission) -> ()` para erro tipado? +- [ ] O worker owns `Gfx`/framebuffer, ou `Gfx` continua em `Hardware` e precisa ser dividido? +- [ ] Como o viewport cache sera exposto ao worker: snapshot por frame, `Arc` read-only, double-buffer, ou service proprietario? +- [ ] Quem roda o present loop de 60Hz: worker, host event loop, ou uma autoridade de display separada? +- [ ] Como testar current work em voo sem depender de sleeps/flakiness? +- [ ] Como mapear shutdown: stop accepting, discard pending, cancel current, join bounded, preserve last surface? +- [ ] Quais tipos precisam de prova `Send + Sync` antes do primeiro worker real? +- [ ] Shell continua fora do worker no primeiro passo, ou devemos provar explicitamente que capability Game nao afeta Shell? + +## Criterio para Encerrar + +Esta agenda pode virar decision quando tivermos uma resposta clara para: + +- ownership/layer responsavel pelo worker real; +- trait/backend de render/present e modelo de erro; +- modelo de recurso read-only, especialmente viewport cache; +- protocolo de shutdown/current work; +- present cadence/repeat; +- plano de testes para thread/core real; +- escopo do primeiro worker real, incluindo o que fica explicitamente fora. + +## Discussion + +- 2026-06-06: Agenda criada apos a execucao de `DSC-0040`, que fechou a arquitetura base mas deixou o worker real como etapa futura deliberada. + +## Resolution diff --git a/discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md b/discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md deleted file mode 100644 index bd1cf38b..00000000 --- a/discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md +++ /dev/null @@ -1,245 +0,0 @@ ---- -id: DEC-0031 -ticket: vm-render-parallel-execution-boundary -title: VM and Render Parallel Execution Boundary -status: accepted -created: 2026-06-05 -ref_agenda: AGD-0040 -tags: [runtime, renderer, vm, concurrency, architecture, perf] ---- - -## Status - -Open decision draft generated from accepted agenda `AGD-0040`. - -This decision is ready for review/acceptance. Once accepted, it becomes the normative contract for any spec or code plan that introduces a real render worker/core. - -## Contexto - -`DSC-0038` established `RenderSubmission` as a closed typed snapshot. That gives Prometeu the data boundary needed for future concurrency, but it did not define the operational contract for running VM/logical execution and render consumption on different threads/cores. - -The current runtime consumes submissions synchronously through local `Hardware`/`Gfx` paths. Moving rasterization/presentation out of the VM path requires explicit rules for packet completeness, shared resources, handoff, pacing, lifecycle ownership, and telemetry. - -## Decisao - -Prometeu SHALL treat VM execution and render consumption as separate responsibilities connected by closed render submissions. - -For the first real asynchronous render model, the runtime SHALL use a single-slot latest-wins handoff for Game pipelines, paced by a frame scheduler, with render ownership guarded by epoch/generation and observable through telemetry. Shell/System UI SHALL remain lifecycle/event-driven and local/synchronous by default, while preserving the same closed-packet boundary. - -This decision does not require implementing the worker immediately. It locks the architecture that any future worker implementation MUST follow. - -## Rationale - -The design preserves the console/game timing model without allowing the VM to depend on render success. It also avoids an unbounded queue, stale frame presentation, mutable access to live VM/renderer state, and accidental treatment of Shell UI as a game renderer. - -The decision keeps the first worker model small: - -- the VM publishes at most one latest pending submission; -- the worker consumes owned immutable submissions; -- the frame scheduler, not render ACK, paces logical Game frames; -- lifecycle transitions invalidate stale visual ownership; -- telemetry reports drops/repeats/errors without changing VM semantics. - -## Invariantes / Contrato - -### 1. Packet completo - -`RenderSubmissionPacket::Game2D` MUST be a complete frame description for the render consumer. - -The Game2D consumer MUST compose in this order: - -```text -Game2D composer / scene / layers / sprites --> gfx2d primitives overlay --> publication / present -``` - -The active-scene path MUST NOT bypass the packet or drop `gfx2d`. `gfx2d` primitives are a final overlay both with and without an active scene. - -### 2. Recursos compartilhados - -Packets crossing the VM/render boundary MUST remain small and owned. Heavy resources MUST NOT be copied into every submission. - -The packet SHALL carry stable IDs/handles. Large resident resources such as glyph banks, scene banks, assets, and viewport cache materializations SHALL be accessed through read-only APIs. - -The viewport cache belongs to the logical core. The render worker MAY read it through a read-only API after logical/core preparation has completed. Resolver updates, cache refreshes, asset installation, and bank residency changes MUST happen before handoff or in an owning service, not during worker rasterization. - -The system does not guarantee visual integrity when a developer/framework swaps resources behind an in-flight submission in a way that violates asset discipline. Submissions refer to resource IDs; disciplined replacement is the responsibility of the developer/framework layer. - -Any implementation that crosses a Rust thread boundary MUST prove the handoff and read-only resource APIs are safe for sharing. The worker MUST NOT receive `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM/runtime state. - -### 3. Handoff - -The base handoff protocol SHALL be single-slot latest-wins. - -The producer moves an owned closed submission into a pending slot. If the worker has not consumed that pending submission and the producer publishes a newer one, the newer submission replaces the older one. Replacement MUST be counted as a dropped/replaced-before-consume event. - -The worker owns any submission it has taken from the slot. A currently-rendering submission is outside the pending slot. - -The VM/producer MUST NOT block waiting for worker ACK, consumption, raster completion, or present completion. Worker consumed/presented status is telemetry only. - -Shell UI is outside the worker protocol by default. A future configuration MAY reuse the same handoff for Shell, but that is not the default policy. - -### 4. Frame pacing - -Game logical frames SHALL be paced by a `FrameClock`/`FrameScheduler` authority, not by render-worker ACK. - -The VM MUST NOT run freely ahead of display cadence. The target is one logical Game frame per frame tick and at most one pending submission ahead of the worker. The worker presents at display cadence, initially 60Hz where available, and repeats the last valid frame if no new valid submission exists. - -`FRAME_SYNC` remains the canonical end of a VM logical frame. The old budget mechanism MUST NOT be used as the normal way to cut or advance a logical frame. Budget/cycle/time accounting remains for certification, watchdog, diagnostics, and overrun reporting. - -If a logical frame exceeds the display window, logical frames remain sequential. The runtime records overrun/stutter and the worker repeats the last valid frame. The delayed submission represents the next sequential logical frame, not a skipped `N+k` frame. - -Input sampling for Game SHOULD occur at the start of an authorized logical frame. Repeated presents without a new logical frame MUST NOT imply new input sampling or a hidden logical update. - -### 5. Politica por AppMode / pipeline - -Render execution policy SHALL be explicit by AppMode/pipeline. - -Game workloads are frame-paced by definition. Current `AppMode::Game` and future explicit Game2D/Game3D modes SHALL use the frame scheduler and MAY use a render worker by default when the host/runtime supports it, with synchronous/local fallback. - -Shell/System UI is lifecycle/event-driven. It MUST NOT inherit the Game logical frame loop. Shell UI SHALL publish local/synchronous by default and render by invalidation/event/lifecycle. Shell VM-backed apps follow Shell lifecycle and MUST NOT declare an independent frame-paced workload under this decision. - -Firmware/system screens such as splash, crash, and hub follow Shell/local policy unless a later decision creates a more specific policy. - -SystemOS/lifecycle decides the semantic profile/AppMode. `RenderManager` executes the render policy. Host/HAL provides capabilities and concrete execution. - -### 6. Lifecycle, epoch e ownership - -Every submission that may cross an asynchronous boundary MUST carry enough ownership context to reject stale presentation. This includes frame identity plus render ownership such as epoch/generation and owner/app/mode identity as needed. - -Any foreground visual-owner transition SHALL increment/apply a render epoch or equivalent generation through a central `RenderManager` API. SystemOS/lifecycle decides that a semantic transition occurred; `RenderManager` applies the render ownership transition. - -Transitions that MUST invalidate stale pending/current submissions include: - -- Game -> Shell/Hub; -- Shell/Hub -> Game; -- crash screen; -- splash or system screen takeover; -- cartridge/app swap, even if `AppMode` remains the same; -- shutdown/stop. - -The worker MAY finish raster work for an obsolete submission, but it MUST check epoch/owner before present and MUST NOT present frames that no longer match active ownership. - -The same physical surface MAY be shared by Game, Shell, Hub, splash, and crash screens, but logical ownership MUST be explicit. On Shell -> Game, the runtime MAY keep the previous Shell/Hub surface visible until the first valid Game submission; clearing is a visual policy, not a correctness requirement. - -Shutdown MUST stop presentation of obsolete work, discard pending submissions, invalidate current work through epoch/stop token, and terminate/join the worker in a bounded way. It MUST NOT drain obsolete frames to the screen. - -Foreground stack, Game pause/resume, and coexistence of paused Game with VM-backed Shell apps are outside this decision and are tracked separately by `DSC-0041`. - -### 7. Erros, drops e telemetria - -Asynchronous render is observable best-effort, not a semantic handshake with the VM. - -Render drops, repeated presents, stale-epoch discards, raster errors, and present errors MUST be reported through runtime/host telemetry. They MUST NOT directly alter VM program semantics, and VM code MUST NOT depend on "this frame was presented" to advance logic. - -Minimum counters: - -- `produced_submissions`; -- `replaced_before_consume`; -- `consumed_submissions`; -- `presented_frames`; -- `repeated_presents`; -- `render_errors`; -- `present_errors`; -- `stale_epoch_discards`; -- `shutdown_discards`. - -Minimum IDs/state: - -- `last_produced_frame_id`; -- `last_consumed_frame_id`; -- `last_presented_frame_id`; -- `last_dropped_frame_id`; -- `last_error_frame_id`; -- `active_render_epoch`. - -Minimum events: - -- submission replaced in the pending slot before consumption; -- worker consumed a submission; -- worker discarded a submission due to stale epoch/owner; -- worker presented a frame; -- worker repeated the last frame because no new valid submission existed; -- raster error; -- present error; -- shutdown/cancel discarded pending/current work. - -Raster error on a submission SHOULD discard that submission, record telemetry, and preserve the last valid frame/current surface when possible. Present errors SHOULD record telemetry and MAY escalate to host/system fault policy if repeated. Worker panic MUST become controlled worker/backend failure and MUST NOT propagate as a VM panic. - -## Impactos - -### Spec - -Specs need to describe: - -- closed render packet completeness; -- handoff and pacing model; -- render ownership epoch/generation; -- AppMode/pipeline policy; -- minimum telemetry surface. - -### Runtime - -`RenderManager` needs to become the central owner of render handoff, policy execution, epoch/generation, and render telemetry. It remains coordinator, not renderer. - -### Host / HAL - -Host/HAL provides concrete execution capability: synchronous/local render, render worker thread, separate core, DMA/copresenter, or fallback. Host/HAL does not decide lifecycle ownership. - -### Firmware / SystemOS - -SystemOS/lifecycle remains the semantic owner of mode/app/task transitions. Firmware state changes must route visual ownership transitions through the render ownership API when asynchronous render exists. - -### Tests - -Plans derived from this decision must include tests for: - -- active-scene Game2D preserving `gfx2d` overlay after layer composition; -- latest-wins replacement telemetry; -- stale epoch discard before present; -- Shell local/default policy remaining independent from Game pacing; -- logical frame overrun producing repeat/stutter telemetry without frame skip semantics. - -## Alternativas Descartadas - -### Fila crescente de submissions - -Rejected because it increases latency, memory usage, and stale frame risk. The first worker model should not accumulate old frames. - -### Worker como dono do frame counter canonico - -Rejected because it mixes pacing authority with raster/present completion. Frame cadence belongs to the frame scheduler; the worker is a consumer/presenter. - -### Shell sempre no render worker - -Rejected because Shell/System UI is lifecycle/event-driven. Forcing worker usage adds complexity without clear benefit. - -### Capturar recursos pesados por frame - -Rejected because it is expensive and makes resource lifetime policy obscure. Packets should carry IDs and use read-only resident resources. - -### ACK de present como parte da semantica da VM - -Rejected because it couples deterministic VM progress to host/render timing and failure modes. - -## Referencias - -- `AGD-0040`: VM and Render Parallel Execution Boundary. -- `DSC-0038` / `LSN-0047`: typed render submissions preserve domain boundaries. -- `DSC-0031` / `LSN-0040`: AppMode separates Game and System/Shell profiles. -- `DSC-0032` / `LSN-0041`: SystemOS owns lifecycle semantics. -- `DSC-0035` / `LSN-0044`: Shell liveness depends on task-owned focused window. -- `DSC-0036` / `LSN-0045`: Hub UI slices prove lifecycle boundaries. -- `DSC-0041`: foreground stack, Game pause, and VM-backed Shell coexistence. - -## Propagacao Necessaria - -- Create a plan before spec or code execution. -- First plan should probably separate immediate packet correctness from future worker infrastructure. -- Spec text must be in English. -- Lessons must be created only after execution publishes spec/code state. - -## Revision Log - -- 2026-06-05: Initial decision draft generated from accepted `AGD-0040`. diff --git a/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md b/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md deleted file mode 100644 index a01d9f07..00000000 --- a/discussion/workflow/plans/PLN-0087-fix-game2d-packet-overlay-composition.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: PLN-0087 -ticket: vm-render-parallel-execution-boundary -title: Fix Game2D Packet Overlay Composition -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, renderer, game2d, packet, overlay] ---- - -## Briefing - -`DEC-0031` requires `RenderSubmissionPacket::Game2D` to be a complete frame description. The current active-scene path can bypass the packet and lose `gfx2d` overlay commands. - -## Objective - -Make Game2D packet consumption identical for active-scene and no-scene frames: scene/layers/sprites first, then `gfx2d` primitives as final overlay. - -## Dependencies - -- Source decision: `DEC-0031`. -- Existing Game2D packet types in `crates/console/prometeu-hal`. -- Existing publish path in `crates/console/prometeu-drivers`. - -## Scope - -- Update Game2D render publication so active-scene rendering consumes the packet and applies `gfx2d`. -- Keep Shell UI behavior unchanged. -- Add regression tests for active scene plus primitive overlay. - -## Non-Goals - -- Do not introduce a render worker. -- Do not change frame pacing, telemetry, lifecycle, or AppMode policy. -- Do not redesign `FrameComposer` beyond what is needed to consume the packet correctly. - -## Execution Method - -1. Inspect `Hardware::publish_render_submission`, `Gfx::render_game2d_frame_packet`, and `FrameComposer::render_frame`. -2. Replace the active-scene branch that bypasses `Game2DFramePacket` with a packet-consuming path. -3. Ensure `gfx2d` commands are applied after scene/layer/sprite composition in all Game2D branches. -4. Add a test fixture with an active scene and a primitive overlay that would visibly overwrite or mark the composed layer. -5. Add a no-scene regression test if current coverage does not already lock the overlay order. - -## Acceptance Criteria - -- Active-scene Game2D frames render `gfx2d` after layer/sprite composition. -- No-scene Game2D frames preserve the existing overlay behavior. -- `Hardware::publish_render_submission` does not discard `packet.gfx2d`. -- Tests fail on the old active-scene bypass behavior. - -## Tests - -- Unit or integration test around Game2D packet publication. -- Pixel/assertion test for overlay-after-scene ordering where practical. -- Existing renderer tests continue to pass. - -## Affected Artifacts - -- `crates/console/prometeu-drivers/src/hardware.rs` -- `crates/console/prometeu-hal/src/render_submission.rs` -- `crates/console/prometeu-system` or renderer test modules as needed. diff --git a/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md b/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md deleted file mode 100644 index 0c7098e9..00000000 --- a/discussion/workflow/plans/PLN-0088-define-read-only-render-resource-boundary.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -id: PLN-0088 -ticket: vm-render-parallel-execution-boundary -title: Define Read-Only Render Resource Boundary -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, renderer, resources, cache, banks] ---- - -## Briefing - -`DEC-0031` requires render packets to carry small owned data and stable resource IDs, while heavy banks/caches remain resident and read-only to render consumers. - -## Objective - -Define and enforce the read-only resource boundary needed before any worker can safely read scene/glyph/cache data. - -## Dependencies - -- Source decision: `DEC-0031`. -- `PLN-0087` should clarify the packet consumer path before this plan finalizes worker-facing APIs. - -## Scope - -- Identify every resource read during Game2D rendering. -- Classify data as packet-owned, resident read-only, or logical-core mutable preparation state. -- Add trait/API boundaries for render read access where missing. -- Document which types must be `Send + Sync` for a real threaded worker. - -## Non-Goals - -- Do not make resource swaps transactional. -- Do not capture `Arc` or `Arc` inside every submission. -- Do not implement background asset loading policy. - -## Execution Method - -1. Trace Game2D render reads from packet to glyph bank, scene bank, viewport cache, and framebuffer helpers. -2. Create a short internal module-level contract for render resource access. -3. Add or refine read-only access traits for banks/cache without exposing mutable runtime state. -4. Add compile-time assertions or targeted tests for `Send + Sync` where a threaded worker will require them. -5. Keep resource identity in submissions as IDs/handles, not heavy ownership captures. - -## Acceptance Criteria - -- Render consumer APIs can be described as packet-owned plus read-only resource access. -- No worker-facing API requires `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state. -- Shared read APIs that cross thread boundaries have explicit `Send + Sync` expectations. -- Existing synchronous renderer behavior remains unchanged. - -## Tests - -- Compile-time trait assertions for read-only resource APIs where practical. -- Unit tests for read-only bank/cache access. -- Existing asset/scene rendering tests continue to pass. - -## Affected Artifacts - -- `crates/console/prometeu-hal` -- `crates/console/prometeu-system` -- `crates/console/prometeu-drivers` -- Relevant renderer/resource tests. diff --git a/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md b/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md deleted file mode 100644 index 760139fc..00000000 --- a/discussion/workflow/plans/PLN-0089-introduce-render-handoff-abstraction.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: PLN-0089 -ticket: vm-render-parallel-execution-boundary -title: Introduce Render Handoff Abstraction -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, renderer, handoff, latest-wins] ---- - -## Briefing - -`DEC-0031` requires a single-slot latest-wins handoff where the producer never blocks on render ACK and replacement before consumption is observable. - -## Objective - -Introduce a render handoff abstraction that can be used synchronously today and by a worker later. - -## Dependencies - -- Source decision: `DEC-0031`. -- `PLN-0093` should define telemetry counters before final instrumentation is wired. - -## Scope - -- Add a small handoff type or trait around one pending owned `RenderSubmission`. -- Support publish, take latest, replace pending, and drain/discard operations. -- Report replacement/drop events to telemetry hooks. -- Integrate the abstraction into `RenderManager` without starting a worker. - -## Non-Goals - -- Do not spawn a thread. -- Do not implement frame pacing. -- Do not change Shell UI default rendering policy. - -## Execution Method - -1. Locate `RenderManager::latest_complete_submission` ownership and publication flow. -2. Introduce a handoff module with single pending slot semantics. -3. Move `RenderSubmission` into the slot on publication. -4. Make `take_latest` transfer ownership to the consumer. -5. Count replacement when a pending submission is overwritten before consumption. -6. Keep current synchronous publication working by immediately taking and rendering through the same abstraction. - -## Acceptance Criteria - -- `RenderManager` no longer relies on an unconstrained raw `Option` for the future worker boundary. -- Publishing a newer pending submission replaces the old one without blocking. -- The consumer receives owned submissions. -- Replacement before consume is observable. - -## Tests - -- Unit tests for empty take, publish/take, publish/replace/take, and drain/discard. -- Integration smoke test that existing synchronous render still presents frames. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs` -- New or existing render handoff module under `crates/console/prometeu-system`. -- Render manager tests. diff --git a/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md b/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md deleted file mode 100644 index 78511780..00000000 --- a/discussion/workflow/plans/PLN-0090-add-render-frame-pacing-contract.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -id: PLN-0090 -ticket: vm-render-parallel-execution-boundary -title: Add Render Frame Pacing Contract -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, frame-clock, scheduler, vm] ---- - -## Briefing - -`DEC-0031` separates Game logical frame pacing from render worker ACK. Game frames are paced by a frame clock/scheduler and still end at `FRAME_SYNC`. - -## Objective - -Define and implement the runtime contract that authorizes Game logical frames without using render present completion as ACK. - -## Dependencies - -- Source decision: `DEC-0031`. -- `PLN-0091` should define which AppModes consume this policy. - -## Scope - -- Introduce or formalize a `FrameClock`/`FrameScheduler` authority for Game workloads. -- Ensure Game VM runs at most one logical frame per authorized frame tick. -- Preserve `FRAME_SYNC` as logical frame end. -- Record overrun/repeat conditions through telemetry hooks. - -## Non-Goals - -- Do not make Shell UI run a game loop. -- Do not implement catch-up frames or logical frame skipping. -- Do not tie VM progress to render ACK. - -## Execution Method - -1. Inspect current tick/budget/`FRAME_SYNC` handling. -2. Identify where a frame tick should authorize Game VM execution. -3. Introduce a narrow scheduling API consumed by Game lifecycle code. -4. Preserve budget/cycle/time accounting only for watchdog, certification, and diagnostics. -5. Add telemetry points for logical-frame-overrun and no-new-frame/repeat. -6. Document the distinction between logical frame ID, produced submission ID, and presented frame ID. - -## Acceptance Criteria - -- Game VM cannot run unlimited logical frames ahead of the frame clock. -- Render worker or render consumer ACK is not required to start the next logical frame. -- Slow logical frames remain sequential and produce overrun/stutter telemetry. -- Shell lifecycle remains unaffected. - -## Tests - -- Unit tests for scheduler permitting one Game frame per tick. -- Test that no catch-up `N+k` logical frame is produced after overrun. -- Regression tests around `FRAME_SYNC` behavior. - -## Affected Artifacts - -- VM runtime scheduling modules. -- Firmware/Game running step. -- Telemetry interfaces touched by pacing. diff --git a/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md b/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md deleted file mode 100644 index 9585762f..00000000 --- a/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -id: PLN-0091 -ticket: vm-render-parallel-execution-boundary -title: Implement AppMode Render Policy Matrix -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, appmode, shell, game, policy] ---- - -## Briefing - -`DEC-0031` requires render execution policy to be explicit by AppMode/pipeline: Game is frame-paced and may use worker; Shell/System UI is lifecycle-driven and local/synchronous by default. - -## Objective - -Add an explicit render policy matrix so Game and Shell rendering cannot accidentally share the wrong lifecycle or pacing model. - -## Dependencies - -- Source decision: `DEC-0031`. -- `PLN-0090` defines the Game pacing contract. - -## Scope - -- Represent render policy for current `AppMode::Game` and `AppMode::Shell`. -- Make policy selection a SystemOS/lifecycle decision executed by `RenderManager`. -- Add host/runtime capability fallback for worker-supported vs local render. -- Keep Shell VM-backed apps under Shell lifecycle. - -## Non-Goals - -- Do not rename `AppMode::Game` to `Game2D` in this plan. -- Do not implement Game3D. -- Do not define Game pause/resume foreground stack; that belongs to `DSC-0041`. - -## Execution Method - -1. Locate `AppMode` routing through cartridge load, firmware state, SystemOS, and `RenderManager`. -2. Add a render policy enum or equivalent explicit matrix. -3. Route Game to frame-paced policy with optional worker capability. -4. Route Shell/system screens to local/synchronous lifecycle policy. -5. Add assertions/tests that Shell UI does not consume Game frame scheduler. - -## Acceptance Criteria - -- Policy is explicit and testable for Game and Shell. -- Shell UI remains local/synchronous by default. -- Game can declare worker-capable policy without requiring the worker to exist. -- Host/runtime capability fallback is represented without moving ownership decisions to host. - -## Tests - -- Unit tests for AppMode-to-render-policy mapping. -- Lifecycle test proving Shell does not require Game frame ticks. -- Regression test for Game still producing frame-paced submissions. - -## Affected Artifacts - -- `AppMode` routing modules. -- `RenderManager`. -- Firmware load/run steps. -- SystemOS lifecycle integration tests. diff --git a/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md b/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md deleted file mode 100644 index 57f05e98..00000000 --- a/discussion/workflow/plans/PLN-0092-add-render-epoch-ownership-transitions.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -id: PLN-0092 -ticket: vm-render-parallel-execution-boundary -title: Add Render Epoch Ownership Transitions -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, lifecycle, render-epoch, ownership] ---- - -## Briefing - -`DEC-0031` requires foreground visual-owner transitions to invalidate stale render work using epoch/generation and owner identity. - -## Objective - -Add render epoch/ownership metadata and transition APIs so stale submissions cannot be presented after mode, app, crash, cartridge, or shutdown transitions. - -## Dependencies - -- Source decision: `DEC-0031`. -- `PLN-0089` provides the handoff abstraction. - -## Scope - -- Add epoch/generation and owner identity to render submission metadata. -- Add central `RenderManager` API for applying ownership transitions. -- Integrate lifecycle transitions that change visual owner. -- Reject stale pending/current submissions before present. - -## Non-Goals - -- Do not implement smooth visual transitions. -- Do not define Game pause/resume stack semantics from `DSC-0041`. -- Do not give host/HAL authority to decide ownership. - -## Execution Method - -1. Define render owner metadata shape: epoch/generation plus mode/app/surface owner as needed. -2. Extend `RenderSubmission` metadata and constructors. -3. Add `RenderManager` transition API that increments/applies epoch. -4. Call the API from SystemOS/firmware lifecycle transition points. -5. Update render consumption to check ownership before present. -6. Add discard telemetry hook for stale epoch. - -## Acceptance Criteria - -- Game -> Shell, Shell -> Game, crash, splash, cartridge/app swap, and shutdown can invalidate stale render work. -- Worker/current consumer cannot present a submission with stale epoch/owner. -- Host/HAL does not decide semantic ownership. -- Same physical surface can be reused with distinct logical ownership. - -## Tests - -- Unit test stale epoch discard before present. -- Integration test mode transition with pending Game submission. -- Test cartridge/app swap increments ownership even when AppMode remains unchanged. - -## Affected Artifacts - -- `RenderSubmission` metadata. -- `RenderManager`. -- Firmware/SystemOS transition code. -- Render publication path. diff --git a/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md b/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md deleted file mode 100644 index 2fd67371..00000000 --- a/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -id: PLN-0093 -ticket: vm-render-parallel-execution-boundary -title: Add Render Telemetry Counters and Events -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, renderer, telemetry, diagnostics] ---- - -## Briefing - -`DEC-0031` requires render drops, repeats, stale discards, render errors, and present errors to be observable without becoming VM semantics. - -## Objective - -Add the minimum render telemetry surface required for asynchronous render diagnostics and certification. - -## Dependencies - -- Source decision: `DEC-0031`. -- `PLN-0089` and `PLN-0092` provide main event sources. - -## Scope - -- Add minimum counters and last-frame IDs from `DEC-0031`. -- Add event recording hooks for handoff, consume, present, repeat, stale discard, errors, and shutdown discard. -- Expose telemetry through existing runtime/host diagnostics patterns. -- Ensure VM program behavior cannot observe telemetry as render ACK. - -## Non-Goals - -- Do not build a UI profiler. -- Do not add VM syscalls for present ACK. -- Do not define host-specific logging formats beyond necessary hooks. - -## Execution Method - -1. Locate current telemetry facilities and atomic counter patterns. -2. Add render telemetry structure with counters and last IDs. -3. Wire replacement-before-consume from handoff. -4. Wire consumed/presented/repeated from consumer path. -5. Wire stale epoch and shutdown discards. -6. Wire render/present error reporting. -7. Add read-only diagnostics access for host/debug tooling. - -## Acceptance Criteria - -- All minimum counters and IDs from `DEC-0031` exist. -- Latest-wins replacement is measurable. -- Repeated presents and stale epoch discards are measurable. -- Telemetry is not a blocking ACK path for the VM. - -## Tests - -- Unit tests for counter increments. -- Handoff replacement telemetry test. -- Stale epoch discard telemetry test. -- Error path telemetry test where render/present errors can be simulated. - -## Affected Artifacts - -- Runtime telemetry modules. -- `RenderManager`. -- Render handoff and publication paths. -- Host/debug telemetry readers. diff --git a/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md b/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md deleted file mode 100644 index 7b0ac40d..00000000 --- a/discussion/workflow/plans/PLN-0094-specify-async-render-boundary.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -id: PLN-0094 -ticket: vm-render-parallel-execution-boundary -title: Specify Async Render Boundary -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [spec, runtime, renderer, architecture] ---- - -## Briefing - -`DEC-0031` needs canonical English spec coverage before worker-oriented code becomes normative. - -## Objective - -Publish the async render boundary contract in the canonical specs location. - -## Dependencies - -- Source decision: `DEC-0031`. -- Should run before large worker implementation plans are executed. - -## Scope - -- Add or update English spec text for packet completeness, handoff, pacing, AppMode policy, epoch ownership, and telemetry. -- Link the spec language to runtime concepts without exposing implementation-only details. -- Keep `DSC-0041` foreground stack details out of this spec pass except as explicit out-of-scope reference. - -## Non-Goals - -- Do not document future Game3D behavior beyond extensibility notes. -- Do not write lessons; lessons happen after execution. -- Do not encode implementation file paths as spec requirements. - -## Execution Method - -1. Locate canonical specs for runtime/render/AppMode. -2. Add a section for asynchronous render boundary or update the closest existing spec. -3. Translate normative parts of `DEC-0031` into concise English spec language. -4. Add out-of-scope note for Game pause/resume and VM-backed Shell coexistence under `DSC-0041`. -5. Cross-check terminology against existing specs. - -## Acceptance Criteria - -- Canonical specs describe the async render boundary in English. -- Spec language covers all seven decision areas. -- No Portuguese text is introduced in specs. -- The spec does not require worker implementation before the plan for it exists. - -## Tests - -- Documentation review. -- `rg` check for accidental Portuguese in touched spec files. -- Existing doc/spec validation scripts if present. - -## Affected Artifacts - -- Canonical specs location in the repository. -- `discussion/workflow/decisions/DEC-0031-vm-and-render-parallel-execution-boundary.md` as reference only. diff --git a/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md b/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md deleted file mode 100644 index 7d0b9d23..00000000 --- a/discussion/workflow/plans/PLN-0095-prototype-local-render-worker-capability.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -id: PLN-0095 -ticket: vm-render-parallel-execution-boundary -title: Prototype Local Render Worker Capability -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, host, renderer, worker, prototype] ---- - -## Briefing - -`DEC-0031` allows Game pipelines to use a render worker when host/runtime capabilities support it, with local/synchronous fallback. - -## Objective - -Prototype the first local render worker capability behind the established handoff, policy, epoch, and telemetry contracts. - -## Dependencies - -- `PLN-0087` through `PLN-0093` should be complete or accepted as prerequisites. -- `PLN-0094` should publish the spec boundary before this becomes production work. - -## Scope - -- Add a host/runtime capability flag for render worker support. -- Spawn a local worker for Game policy only. -- Consume the single-slot handoff. -- Present at display cadence or the nearest available host cadence. -- Preserve synchronous/local fallback. - -## Non-Goals - -- Do not optimize performance beyond proving the contract. -- Do not enable Shell worker by default. -- Do not implement hardware-specific core/DMA behavior. - -## Execution Method - -1. Add capability wiring for worker-supported vs local render. -2. Create worker lifecycle owned by runtime/host integration according to `RenderManager` policy. -3. Make worker consume owned submissions through the handoff. -4. Check epoch/owner before present. -5. Emit telemetry for consume, present, repeat, stale discard, and errors. -6. Keep a build/runtime path that uses the existing synchronous renderer. - -## Acceptance Criteria - -- Game rendering can run through a local worker in a controlled configuration. -- Shell rendering remains local/synchronous by default. -- Worker does not access live mutable VM, `Hardware`, `Gfx`, or `FrameComposer` state outside approved boundaries. -- Fallback to synchronous/local render remains available. - -## Tests - -- Worker smoke test with Game submission. -- Fallback mode smoke test. -- Stale epoch discard test while worker is active. -- Thread shutdown test covered with `PLN-0096`. - -## Affected Artifacts - -- Host desktop runtime integration. -- `RenderManager`. -- Render handoff. -- Render backend/driver integration points. diff --git a/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md b/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md deleted file mode 100644 index b6a45125..00000000 --- a/discussion/workflow/plans/PLN-0096-define-render-shutdown-and-failure-handling.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -id: PLN-0096 -ticket: vm-render-parallel-execution-boundary -title: Define Render Shutdown and Failure Handling -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, renderer, shutdown, errors] ---- - -## Briefing - -`DEC-0031` requires shutdown to stop obsolete presentation, discard pending/current work, and terminate worker execution in a bounded way. Worker panic must become controlled failure, not VM panic. - -## Objective - -Define and implement render shutdown, cancellation, and failure handling for both synchronous and worker-capable paths. - -## Dependencies - -- `PLN-0089` handoff abstraction. -- `PLN-0092` epoch/ownership transitions. -- `PLN-0093` telemetry. -- `PLN-0095` if a real worker is being exercised. - -## Scope - -- Add stop token or equivalent cancellation state for worker/current render work. -- Discard pending submissions on shutdown. -- Prevent obsolete current work from presenting after shutdown/owner change. -- Convert worker panic/render backend failure into controlled runtime/host error state. -- Record shutdown discards and errors in telemetry. - -## Non-Goals - -- Do not design full crash UI policy beyond render ownership requirements. -- Do not implement automatic worker restart unless existing host policy already supports it. -- Do not expose failure ACK to VM program semantics. - -## Execution Method - -1. Define shutdown state machine for render consumer. -2. Add bounded worker termination behavior where worker exists. -3. Add pending/current discard paths. -4. Ensure present path checks stop/epoch before publishing. -5. Wrap worker entrypoint with panic/error containment. -6. Add telemetry for shutdown discards, render errors, present errors, and worker failure. - -## Acceptance Criteria - -- Shutdown does not drain obsolete frames to screen. -- Pending work is discarded. -- Current work cannot present after stop/epoch invalidation. -- Worker failure is reported without panicking the VM. -- Shutdown completes in bounded time. - -## Tests - -- Unit test pending discard on shutdown. -- Integration test shutdown during currently-rendering submission. -- Simulated worker panic/error test. -- Telemetry assertions for shutdown/error counters. - -## Affected Artifacts - -- Render handoff. -- Worker runtime integration. -- `RenderManager`. -- Telemetry modules. -- Host error handling. diff --git a/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md b/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md deleted file mode 100644 index 18e007ea..00000000 --- a/discussion/workflow/plans/PLN-0097-add-async-render-integration-test-matrix.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -id: PLN-0097 -ticket: vm-render-parallel-execution-boundary -title: Add Async Render Integration Test Matrix -status: done -created: 2026-06-05 -ref_decisions: [DEC-0031] -tags: [runtime, renderer, tests, integration] ---- - -## Briefing - -`DEC-0031` spans packet correctness, resource boundaries, handoff, pacing, AppMode policy, epoch ownership, telemetry, and failure behavior. These need integration coverage across plans. - -## Objective - -Create an integration test matrix that proves the async render boundary contracts work together and prevents regressions while plans are implemented incrementally. - -## Dependencies - -- Source decision: `DEC-0031`. -- This plan can start early as test scaffolding and expand as other plans land. - -## Scope - -- Define a test matrix mapping each `DEC-0031` invariant to at least one test. -- Add reusable fixtures for Game2D submissions, Shell UI submissions, stale epoch, handoff replacement, and frame repeat. -- Ensure tests run in local/synchronous mode and, when available, worker mode. - -## Non-Goals - -- Do not require real hardware backend. -- Do not require visual screenshot testing unless the existing test stack already supports it. -- Do not test `DSC-0041` foreground stack semantics. - -## Execution Method - -1. Build a matrix with rows for each decision invariant. -2. Identify existing tests that already cover rows. -3. Add missing tests as focused runtime/renderer integration tests. -4. Add helpers to construct deterministic submissions and resource IDs. -5. Run matrix in synchronous mode first. -6. Add worker-mode variants when `PLN-0095` exists. - -## Acceptance Criteria - -- Each `DEC-0031` invariant has at least one automated or explicitly documented validation. -- Test fixtures distinguish Game and Shell policies. -- Tests cover stale epoch discard, latest-wins replacement, repeated present, render error telemetry, and active-scene overlay. -- The matrix is referenced by later implementation plans. - -## Tests - -- This plan is itself test-focused. -- Required categories: packet composition, handoff, pacing, AppMode policy, epoch, telemetry, shutdown/failure. - -## Affected Artifacts - -- Runtime/renderer integration tests. -- Test fixtures/helpers. -- Any existing CI test grouping that needs to include the new tests. From 80a36a3e7a4c85c141a3b310f7913f2e7354309a Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 6 Jun 2026 13:03:42 +0100 Subject: [PATCH 16/47] Real Render Worker Establishment --- discussion/index.ndjson | 4 +- ...st-hardware-render-boundary-preparation.md | 191 +++++++++++++++++ ...D-0042-real-render-worker-establishment.md | 172 --------------- ...D-0043-real-render-worker-establishment.md | 153 ++++++++++++++ ...rm-layer-and-hardwarebridge-elimination.md | 198 ++++++++++++++++++ ...N-0098-define-platform-facade-contracts.md | 81 +++++++ ...-introduce-owned-render-submission-sink.md | 82 ++++++++ ...igrate-local-render-publication-to-sink.md | 80 +++++++ ...-remove-immediate-gfx-syscall-rendering.md | 82 ++++++++ ...introduce-game2d-frame-composer-service.md | 82 ++++++++ ...oduce-runtime-platform-and-testplatform.md | 82 ++++++++ ...rate-vm-runtime-hostcontext-to-platform.md | 79 +++++++ ...e-firmware-and-hub-to-platform-services.md | 81 +++++++ ...grate-desktop-host-to-platform-services.md | 79 +++++++ ...0107-migrate-remaining-platform-domains.md | 89 ++++++++ .../PLN-0108-migrate-tests-to-testplatform.md | 81 +++++++ ...-remove-hardwarebridge-and-update-specs.md | 91 ++++++++ 17 files changed, 1533 insertions(+), 174 deletions(-) create mode 100644 discussion/workflow/agendas/AGD-0042-host-hardware-render-boundary-preparation.md delete mode 100644 discussion/workflow/agendas/AGD-0042-real-render-worker-establishment.md create mode 100644 discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md create mode 100644 discussion/workflow/decisions/DEC-0032-platform-layer-and-hardwarebridge-elimination.md create mode 100644 discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md create mode 100644 discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md create mode 100644 discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md create mode 100644 discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md create mode 100644 discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md create mode 100644 discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md create mode 100644 discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md create mode 100644 discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md create mode 100644 discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md create mode 100644 discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md create mode 100644 discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md create mode 100644 discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 852a7508..d8d12dae 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":43,"AGD":43,"DEC":32,"PLN":98,"LSN":49,"CLSN":1}} +{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":33,"PLN":110,"LSN":49,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"open","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-06","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-real-render-worker-establishment.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"open","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-06","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/agendas/AGD-0042-host-hardware-render-boundary-preparation.md b/discussion/workflow/agendas/AGD-0042-host-hardware-render-boundary-preparation.md new file mode 100644 index 00000000..5bb4f48c --- /dev/null +++ b/discussion/workflow/agendas/AGD-0042-host-hardware-render-boundary-preparation.md @@ -0,0 +1,191 @@ +--- +id: AGD-0042 +ticket: real-render-worker-establishment +title: Host Hardware and Render Boundary Preparation +status: accepted +created: 2026-06-06 +resolved: +decision: DEC-0032 +tags: [runtime, renderer, hardware, host, hal, boundary, architecture] +--- + +## Contexto + +`DSC-0040` fechou a arquitetura base para separar VM/logical execution de render consumption. A implementacao atual ainda usa `HardwareBridge` como uma trait agregadora grande: render, composer, `Gfx`, audio, input, touch e assets ficam expostos juntos para runtime, firmware e syscalls. + +Para estabelecer um worker real, precisamos antes preparar essa fronteira. A direcao agora nao e mais transformar `HardwareBridge` em uma interface host-implemented permanente. A direcao e reformular o runtime em uma platform layer com servicos explicitos e eliminar completamente o `HardwareBridge` monolitico como contrato final. + +O worker real deve depender apenas de uma sub-abstracao minima de render, nunca de `&mut Hardware` inteiro. + +Esta agenda (`AGD-0042`) discute a preparacao/refatoracao de fronteiras. A agenda seguinte (`AGD-0043`) dentro da mesma `DSC-0042` discutira o estabelecimento do worker real propriamente dito. + +## Problema + +Hoje o sistema ja tem uma trait (`HardwareBridge`), mas ela ainda permite acoplamentos que impedem um worker real limpo: + +- `HostContext` entrega `&mut dyn HardwareBridge` para syscalls; +- syscalls `gfx2d.*` e `gfxui.*` ainda fazem buffering de comandos e tambem desenham imediatamente via `hw.gfx_mut()`; +- composer state (`bind_scene`, `set_camera`, `emit_sprite`, `close_game2d_packet`) vive dentro da mesma abstracao que render/present; +- firmware e Hub publicam diretamente via `hw.publish_render_submission`; +- host desktop instancia `prometeu_drivers::Hardware` diretamente e usa constantes concretas como `Hardware::W/H`; +- testes dependem fortemente do `Hardware` concreto. + +Se introduzirmos o worker real agora, a implementacao provavelmente acabara passando `&mut Hardware`, `&mut Gfx` ou `FrameComposer` vivo para a fronteira do worker por conveniencia. Isso quebraria o contrato definido em `DEC-0031`. + +## Pontos Criticos + +### 0. Escopo da migracao completa + +Podemos migrar render primeiro para reduzir risco e desbloquear o worker real, mas a mudanca nao deve parar no render. Audio, input, touch, assets/storage, clock/pacing e telemetry tambem devem sair do `HardwareBridge` monolitico para servicos/facades explicitos. + +`HardwareBridge` deve morrer ao final desta mudanca. Qualquer uso intermediario e scaffold de migracao, nao contrato final. + +### 1. Eliminacao do `HardwareBridge` + +`HardwareBridge` pode existir apenas como detalhe transitorio durante a migracao, se isso reduzir risco de implementacao. Ele nao deve sobreviver como compat layer permanente nem como contrato publico do runtime. + +O estado final deve remover `HardwareBridge` e substituir o acesso monolitico por servicos explicitos de plataforma. + +### 2. Render submission sink + +O primeiro corte provavelmente deve extrair algo como `RenderSubmissionSink`, separando `submit/publish` de `HardwareBridge`. + +### 3. Remocao de render imediato das syscalls + +As syscalls de `gfx2d`/`gfxui` devem apenas gravar comandos nos buffers que fecham o packet. O desenho imediato via `hw.gfx_mut()` deve sair antes do worker real. + +### 4. Composer como dominio logico + +`bind_scene`, `set_camera`, `emit_sprite` e `close_game2d_packet` pertencem ao fechamento logico do frame. Precisamos separar essa capacidade do backend que rasteriza/presenta. + +### 5. Host-owned concrete hardware + +`prometeu_drivers::Hardware` deve continuar existindo como implementacao local/default, mas nao como a arquitetura. O host desktop pode owns essa implementacao ou uma composicao de facades. + +### 6. Test migration + +Muitos testes instanciam `Hardware::new()`. A migracao deve preservar fixtures simples, talvez com um `TestHardware` ou `LocalHardware` que implemente as novas facades. + +## Opcoes + +### Opcao A - Quebrar `HardwareBridge` em uma unica grande refatoracao + +**Abordagem:** substituir `HardwareBridge` por facades menores em runtime, firmware, host e testes de uma vez. + +**Pros:** +- resultado final mais limpo imediatamente; +- remove rapidamente o risco de acoplamento. + +**Contras:** +- alto blast radius; +- muitos testes mudam ao mesmo tempo; +- dificil isolar regressao funcional de regressao arquitetural. + +**Manutenibilidade:** boa no destino, arriscada durante a transicao. + +### Opcao B - Extrair facades incrementalmente e eliminar `HardwareBridge` no final + +**Abordagem:** criar facades menores (`RenderSubmissionSink`, composer/logical frame facade, audio/input/assets), usar `HardwareBridge` apenas como scaffold temporario se necessario, migrar callers por etapas e remover `HardwareBridge` ao final. + +**Pros:** +- menor risco; +- permite commits pequenos; +- preserva testes e host desktop enquanto a fronteira e endurecida; +- facilita medir quando o worker real ja pode nascer. + +**Contras:** +- periodo intermediario com duas camadas; +- exige disciplina para nao continuar usando `gfx_mut()` no caminho errado. +- requer criterio explicito de remocao para evitar que o scaffold vire legado. + +**Manutenibilidade:** melhor equilibrio para o estado atual do repo. + +### Opcao C - Criar apenas `RenderSubmissionSink` agora e adiar o resto + +**Abordagem:** extrair somente a publicacao de submissions e manter composer/gfx/audio/input/assets no `HardwareBridge` por enquanto. + +**Pros:** +- menor mudanca inicial; +- desbloqueia parte do worker path. + +**Contras:** +- ainda deixa syscalls e composer acoplados ao hardware grande; +- worker real ainda pode esbarrar no viewport cache/composer vivo; +- risco de adiar o problema central. + +**Manutenibilidade:** aceitavel como primeiro PR, insuficiente como preparacao completa. + +## Sugestao / Recomendacao + +Recomendo a **Opcao B**, em fases, com uma restricao forte: **`HardwareBridge` deve ser eliminado no estado final**. + +1. Criar `RenderSubmissionSink` com erro tipado minimo, mantendo implementacao local em `prometeu_drivers::Hardware`. +2. Trocar publication local para depender de `RenderSubmissionSink`, nao do `HardwareBridge` inteiro. +3. Remover writes imediatos de `gfx_mut()` nas syscalls `gfx2d`/`gfxui`; syscalls devem apenas bufferizar comandos. +4. Extrair uma facade de composer/logical frame closure para `bind_scene`, `set_camera`, `emit_sprite`, `close_game2d_packet`. +5. Migrar firmware, Hub, runtime, host desktop e testes para as facades novas. +6. Remover `HardwareBridge` e qualquer dependencia nova dele. +7. Atualizar fixtures/testes para dependerem das facades certas. + +O criterio tecnico: antes da agenda do worker real virar plano, nenhum caminho de render worker deve precisar de `&mut Hardware`, `&mut Gfx`, `FrameComposer` vivo ou estado mutavel da VM. + +O criterio arquitetural: antes desta agenda fechar, `HardwareBridge` deve estar definido como artefato a ser removido, nao como compatibilidade preservada. + +## Perguntas em Aberto + +- [x] `HardwareBridge` deve sobreviver como compatibilidade final? + - Resposta: nao. Ele deve ser completamente eliminado no destino. +- [x] Se houver transicao incremental, `HardwareBridge` deve herdar facades menores ou apenas expor accessors temporarios? + - Resposta: se existir durante a migracao, deve usar accessors/adapters temporarios explicitos. Nao deve herdar facades menores, para nao parecer o novo contrato arquitetural. +- [x] `RenderSubmissionSink` deve receber `&RenderSubmission` ou owned `RenderSubmission`? + - Resposta: owned `RenderSubmission`. O contrato deve nascer pronto para handoff/worker; o caminho local pode adaptar internamente quando necessario. +- [x] O erro tipado minimo entra ja nesta preparacao ou fica para a agenda do worker real? + - Resposta: entra ja nesta preparacao, em forma minima. A API nao deve nascer infalivel para ser quebrada logo na agenda do worker real. +- [x] Qual e a primeira syscall/teste que prova que `gfx2d` nao desenha mais imediatamente? + - Resposta: comecar por `gfx2d.clear` e `gfx2d.draw_text`. Os testes devem provar que a syscall apenas bufferiza comando e que pixels mudam somente apos fechamento/publicacao da submission. +- [x] Composer deve ficar no runtime/logical side ou continuar em `prometeu_drivers::Hardware` enquanto nao houver worker? + - Resposta: deve ficar no logical side/runtime service. A migracao pode ser faseada, mas `bind_scene`, `set_camera`, `emit_sprite` e `close_game2d_packet` sao preparacao de frame logico, nao backend fisico. +- [x] Como preservar testes simples que hoje usam `Hardware::new()`? + - Resposta: criar `TestPlatform` como fixture explicita para testes. Nao preservar `HardwareBridge` como compatibilidade. +- [x] Que nomes vamos usar: `Hardware`, `HostHardware`, `LocalHardware`, `RuntimeHardware`, `RenderDevice`, `RenderSubmissionSink`? + - Resposta: usar `Platform`/`RuntimePlatform` para o agregado de servicos, `TestPlatform` para fixtures de teste, `RenderSubmissionSink` para submissao, `RenderBackend` para raster/present, e `Game2DFrameComposer` para o composer logico de Game2D. Evitar `Hardware` como nome arquitetural central. +- [x] Onde os contratos da platform layer devem viver? + - Resposta: contratos/facades em `prometeu-hal`; implementacoes locais/test em `prometeu-drivers`; integracao e ownership operacional no `prometeu-system` e nos hosts. +- [x] `Game2DFrameComposer` pertence ao runtime/logical side ou ao backend de render? + - Resposta: pertence ao logical side. O contrato deve ficar separado do backend renderizavel. A implementacao inicial pode migrar em fases, mas nao deve continuar sendo tratada como parte do hardware fisico. +- [x] Qual e o criterio para remover `HardwareBridge`? + - Resposta: remover quando runtime, firmware, Hub, host desktop e testes tiverem migrado para facades explicitas. A remocao deve ser plano proprio ou criterio final de um plano de migracao, nao compatibilidade opcional. +- [x] Aceitamos a quebra intencional de render imediato em `gfx2d`/`gfxui`? + - Resposta: sim. Syscalls devem bufferizar comandos; pixels devem mudar somente no fechamento/publicacao da submission. Testes que dependem de desenho imediato devem ser atualizados para o novo contrato. + +## Criterio para Encerrar + +Esta agenda pode virar decision quando tivermos: + +- sequencia clara de refatoracao para preparar a fronteira; +- definicao das facades minimas; +- estrategia para remover render imediato das syscalls; +- decisao de remocao do `HardwareBridge` e estrategia de transicao, se houver; +- estrategia de testes/fixtures; +- criterio objetivo de pronto para iniciar `AGD-0043`. + +## Discussion + +- 2026-06-06: Agenda reescopada. A `DSC-0042` passa a ter duas agendas: esta para preparacao da fronteira host/hardware/render, e `AGD-0043` para o worker real. +- 2026-06-06: Direcao levantada: `Hardware` deve ser uma abstracao/contrato implementado pelo host, nao um objeto concreto owned pelo runtime que cruza a fronteira do worker. +- 2026-06-06: Avaliacao inicial de impacto: mudanca media/alta, mas fatiavel. O acoplamento principal esta em `HardwareBridge`, `HostContext`, syscalls de `dispatch`, firmware, Hub, host desktop e testes. +- 2026-06-06: Reformulacao de objetivo: a intencao inicial era expor `Hardware` como interface consumida pelo runtime e implementada pelo host. Com a evolucao do sistema, isso pode estar limitado demais. O novo objetivo e reformular o runtime para um padrao mais proximo de uma plataforma handheld: runtime/OS como dono de lifecycle, scheduling, recursos e contratos; host/board support package como implementacao de dispositivos; render/audio/input/assets expostos por facades separadas e testaveis, nao por uma unica abstracao monolitica de hardware. +- 2026-06-06: Direcao cravada: `HardwareBridge` nao deve ser preservado como compatibilidade. No estado final da reformulacao ele deve ser completamente eliminado, substituido por servicos/facades explicitos de platform layer. +- 2026-06-06: Perguntas abertas respondidas: transicao por adapters/accessors temporarios, `RenderSubmissionSink` owned, erro tipado minimo ja na preparacao, primeiros testes em `gfx2d.clear`/`gfx2d.draw_text`, composer no logical side, fixtures via `TestPlatform`, e nomes preferidos `Platform`/`RuntimePlatform`, `RenderSubmissionSink`, `RenderBackend`, `Game2DFrameComposer`. +- 2026-06-06: Escopo confirmado: render pode migrar primeiro, mas a migracao deve cobrir todos os dominios atualmente presos ao `HardwareBridge`. O objetivo final inclui a remocao completa do `HardwareBridge`, nao apenas reduzir seu uso no render. +- 2026-06-06: Pontos adicionais aceitos: facades em `prometeu-hal`, implementacoes local/test em `prometeu-drivers`, integracao em `prometeu-system`/hosts; `Game2DFrameComposer` no logical side; `HardwareBridge` removido apos migracao completa dos callers; e quebra intencional do desenho imediato das syscalls `gfx2d`/`gfxui`. + +## Resolution + +`AGD-0042` resolve que o destino arquitetural nao e uma interface monolitica `HardwareBridge` implementada pelo host. O destino e uma platform layer composta por servicos/facades explicitos, com contratos em `prometeu-hal`, implementacoes local/test em `prometeu-drivers`, e integracao operacional em `prometeu-system` e nos hosts. + +`HardwareBridge` pode existir apenas como scaffold temporario de migracao. Ele deve ser completamente eliminado ao final, sem compatibilidade permanente. A migracao pode comecar por render, mas deve cobrir todos os dominios atualmente presos ao bridge: render, composer/frame, audio, input/touch, assets/storage, clock/pacing e telemetry quando aplicavel. + +O primeiro corte deve preparar o caminho do render worker sem implementa-lo ainda: introduzir `RenderSubmissionSink` owned com erro tipado minimo, remover desenho imediato das syscalls `gfx2d`/`gfxui`, mover `Game2DFrameComposer` para o logical side/runtime service, e substituir fixtures baseadas em `Hardware::new()` por `TestPlatform`. + +Esta agenda estara pronta para decision quando a decision puder definir a sequencia de migracao e os criterios de remocao final do `HardwareBridge` sem reabrir o contrato de render worker da `DSC-0040`. diff --git a/discussion/workflow/agendas/AGD-0042-real-render-worker-establishment.md b/discussion/workflow/agendas/AGD-0042-real-render-worker-establishment.md deleted file mode 100644 index 944cbf7f..00000000 --- a/discussion/workflow/agendas/AGD-0042-real-render-worker-establishment.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -id: AGD-0042 -ticket: real-render-worker-establishment -title: Real Render Worker Establishment -status: open -created: 2026-06-06 -resolved: -decision: -tags: [runtime, renderer, worker, concurrency, host, hal, architecture] ---- - -## Contexto - -`DEC-0031` fechou a arquitetura base da fronteira VM/render: submissions fechadas, handoff single-slot latest-wins, pacing por `FrameScheduler`, politica explicita por `AppMode`, ownership/epoch e telemetria. - -A implementacao atual ja tem a base local/sincrona e um `LocalRenderWorker` cooperativo por capability flag. Isso valida o roteamento e a politica, mas ainda nao estabelece um worker real em outra thread/core. O proximo passo arquitetural e transformar esse prototipo em uma unidade de execucao real sem reabrir a decisao base. - -Esta agenda existe para discutir somente o estabelecimento do worker real. Ela nao deve redecidir `gfx2d` como overlay, o packet boundary, AppMode policy, ou latest-wins; esses pontos pertencem a `DEC-0031`. - -## Problema - -Um worker real precisa cruzar fronteiras que o prototipo local ainda evita: - -- ownership e lifetime de recursos compartilhados entre VM/logical core e render worker; -- como o worker recebe, consome, rasteriza e apresenta submissions sem `&mut Hardware`, `&mut Gfx`, `FrameComposer` vivo ou estado mutavel da VM; -- como o viewport cache/read model fica seguro e barato; -- como shutdown, stop token, current work e stale owner funcionam quando ha trabalho realmente em voo; -- onde fica o present loop e se ele roda a 60Hz independentemente da VM; -- como erros de render/present deixam de ser apenas panic containment e viram resultado tipado; -- como host desktop, hardware proprio, outro core, DMA ou coprocessador mapeiam para o mesmo contrato. - -## Pontos Criticos - -### 1. Ownership do worker - -Precisamos decidir se o worker real pertence ao runtime, ao host, a HAL, ou a uma camada hibrida. `DEC-0031` diz que host/HAL fornece capability concreta, mas `RenderManager` coordena contrato. Falta definir o boundary operacional. - -### 2. Surface e backend renderizavel - -O worker nao pode receber `&mut Hardware` ou `&mut Gfx` da VM thread. Precisamos de um objeto/capability renderizavel que seja owned pelo worker ou mediado por host. - -### 3. Recursos read-only e viewport cache - -Glyph/scene banks ja tem boundary read-only inicial. O viewport cache ainda precisa de um modelo concreto: snapshot, `Arc` read-only, double-buffer cache, cache materializado por frame, ou service proprietario. - -### 4. Current work e cancelamento - -O `RenderManager` ja descarta pending e rejeita stale epoch antes de present. Com worker real, uma submission pode estar em rasterizacao. Precisamos definir stop token, epoch check antes de present, bounded join e descarte de current work. - -### 5. Present loop e frame repeat - -Hoje repeated present e telemetria/hook. Um worker real provavelmente precisa de loop de present/display cadence. Falta decidir se o worker apresenta a 60Hz, se o host faz scanout/repeat, e como isso conversa com `FrameScheduler`. - -### 6. Erros tipados - -`RenderSurface` hoje e infalivel e panic containment vira `PresentFailed`. Para worker real, render/present deveria retornar erro tipado, distinguindo falha recuperavel, backend lost, surface lost, panic, shutdown e stale discard. - -### 7. Testabilidade - -Precisamos de testes que provem thread boundary, latest-wins sob atraso real, shutdown com current work em voo, stale epoch durante rasterizacao, repeat do ultimo frame e ausencia de bloqueio da VM. - -## Opcoes - -### Opcao A - Worker real dentro do runtime - -**Abordagem:** `RenderManager` ou um novo `RenderWorkerRuntime` cria e gerencia uma thread de render, canais/slots, stop token e lifecycle. - -**Pros:** -- contrato fica perto de `RenderManager`; -- facil testar com runtime puro; -- menos dependencia de host desktop. - -**Contras:** -- risco de runtime assumir detalhes de surface/window que pertencem ao host; -- pode ficar ruim para hardware proprio, DMA ou coprocessador; -- exige abstrair muito bem o backend renderizavel. - -**Manutenibilidade:** boa se o backend for uma trait pequena e owned pelo worker; ruim se vazar `Hardware`/`Gfx` vivo. - -### Opcao B - Worker real como responsabilidade do host - -**Abordagem:** runtime fecha submissions e handoff; host implementa thread/core/present loop usando uma capability HAL/host. - -**Pros:** -- encaixa melhor desktop windowing, swapchain, surface lost e display cadence; -- facilita mapeamento para plataformas diferentes; -- runtime continua sem detalhe de thread/window. - -**Contras:** -- testes de contrato podem ficar mais integrados e menos unitarios; -- risco de cada host reinterpretar o contrato; -- `RenderManager` precisa expor hooks suficientes sem virar passivo demais. - -**Manutenibilidade:** boa para plataformas diversas, desde que exista uma suite de contrato obrigatoria. - -### Opcao C - Worker como HAL/backend owned, coordenado pelo runtime - -**Abordagem:** runtime possui `RenderManager` e policy; HAL/host fornece um `RenderBackend`/`RenderWorkerBackend` owned e thread-safe. Um `RenderWorkerController` conecta handoff, stop token, epoch e telemetry. - -**Pros:** -- separa contrato runtime de implementacao de host; -- permite worker thread no desktop e outro core/DMA em hardware; -- cria ponto unico para testes de contrato; -- evita passar `&mut Hardware` ou `&mut Gfx` pela fronteira. - -**Contras:** -- mais desenho inicial; -- exige definir traits novas com cuidado; -- pode ser excessivo se o primeiro worker real for apenas desktop. - -**Manutenibilidade:** melhor direcao se quisermos preservar portabilidade e nao acoplar runtime a winit/surface. - -### Opcao D - Adiar worker real e manter prototipo local - -**Abordagem:** nao implementar worker real agora; apenas fortalecer testes/spec e manter `LocalRenderWorker`. - -**Pros:** -- menor risco imediato; -- evita mexer em host/surface antes da necessidade real. - -**Contras:** -- nao valida thread/core real; -- deixa lacunas de Send/Sync, shutdown current work e present cadence; -- pode mascarar problemas de ownership ate tarde. - -**Manutenibilidade:** aceitavel por curto prazo, fraca se a demanda agora e paralelismo real. - -## Sugestao / Recomendacao - -Recomendo seguir pela **Opcao C - Worker como HAL/backend owned, coordenado pelo runtime**. - -O desenho deve manter `RenderManager` como dono de policy, epoch, handoff e telemetry, mas introduzir uma camada operacional para worker real com: - -- backend/render surface owned pelo worker ou pelo host; -- handoff single-slot sem bloquear VM; -- stop token e bounded shutdown; -- check de epoch/owner antes de present; -- erro tipado para render/present; -- present cadence explicita, com repeat do ultimo frame quando nao houver submission nova; -- testes de contrato que rodem sem janela nativa sempre que possivel. - -Essa recomendacao preserva `DEC-0031` e evita transformar o runtime puro em dono de detalhes de window/swapchain. - -## Perguntas em Aberto - -- [ ] O primeiro worker real deve nascer no host desktop/winit ou em uma abstracao runtime/HAL testavel sem janela? -- [ ] Qual trait substitui `RenderSurface::consume_submission(&RenderSubmission) -> ()` para erro tipado? -- [ ] O worker owns `Gfx`/framebuffer, ou `Gfx` continua em `Hardware` e precisa ser dividido? -- [ ] Como o viewport cache sera exposto ao worker: snapshot por frame, `Arc` read-only, double-buffer, ou service proprietario? -- [ ] Quem roda o present loop de 60Hz: worker, host event loop, ou uma autoridade de display separada? -- [ ] Como testar current work em voo sem depender de sleeps/flakiness? -- [ ] Como mapear shutdown: stop accepting, discard pending, cancel current, join bounded, preserve last surface? -- [ ] Quais tipos precisam de prova `Send + Sync` antes do primeiro worker real? -- [ ] Shell continua fora do worker no primeiro passo, ou devemos provar explicitamente que capability Game nao afeta Shell? - -## Criterio para Encerrar - -Esta agenda pode virar decision quando tivermos uma resposta clara para: - -- ownership/layer responsavel pelo worker real; -- trait/backend de render/present e modelo de erro; -- modelo de recurso read-only, especialmente viewport cache; -- protocolo de shutdown/current work; -- present cadence/repeat; -- plano de testes para thread/core real; -- escopo do primeiro worker real, incluindo o que fica explicitamente fora. - -## Discussion - -- 2026-06-06: Agenda criada apos a execucao de `DSC-0040`, que fechou a arquitetura base mas deixou o worker real como etapa futura deliberada. - -## Resolution diff --git a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md new file mode 100644 index 00000000..18b054da --- /dev/null +++ b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md @@ -0,0 +1,153 @@ +--- +id: AGD-0043 +ticket: real-render-worker-establishment +title: Real Render Worker Establishment +status: open +created: 2026-06-06 +resolved: +decision: +tags: [runtime, renderer, worker, concurrency, host, hal, architecture] +--- + +## Contexto + +`DEC-0031` definiu o contrato arquitetural da fronteira VM/render. `AGD-0042` prepara a fronteira host/hardware/render para que `Hardware` deixe de ser um objeto concreto atravessando o worker e passe a ser contrato/facades implementadas pelo host. + +Esta agenda discute a implementacao do worker real propriamente dito, assumindo que a preparacao da fronteira foi ou sera resolvida antes da execucao. + +## Problema + +O `LocalRenderWorker` atual e cooperativo/local. Ele prova o roteamento de policy, mas nao prova: + +- thread/core real; +- ownership do backend renderizavel; +- present loop; +- stop token para current work; +- bounded shutdown/join; +- stale epoch durante rasterizacao; +- erro tipado de render/present; +- repeat real do ultimo frame valido; +- ausencia de bloqueio da VM sob atraso do render. + +## Pontos Criticos + +### 1. Ownership e lifecycle do worker + +Precisamos decidir quem cria, inicia, para e reinicia o worker: runtime controller, host/HAL, ou ambos em contrato dividido. + +### 2. Handoff real + +O single-slot latest-wins ja existe como abstracao local. O worker real precisa de uma implementacao thread-safe, sem fila crescente e sem bloquear a VM. + +### 3. Backend renderizavel + +O worker deve owns um backend minimo ou receber capability host-owned. Esse backend deve consumir `RenderSubmission`, resolver recursos read-only e produzir/presentar frame sem `&mut Hardware`. + +### 4. Present cadence + +Precisamos decidir se o worker roda a 60Hz, se o host event loop dirige present, ou se ha uma autoridade separada de display cadence. Repeated present deve virar comportamento real, nao apenas hook. + +### 5. Shutdown/current work + +Quando houver uma submission em rasterizacao, shutdown e owner transition precisam impedir present obsoleto e terminar em tempo bounded. + +### 6. Error model + +Precisamos trocar o modelo infalivel/panic containment por erro tipado para render/present/worker failure. + +### 7. Testes de concorrencia + +Precisamos provar o contrato sem depender de janela nativa nem sleeps frageis. + +## Opcoes + +### Opcao A - Worker thread no runtime com backend mockavel + +**Abordagem:** runtime cria um worker thread generico que recebe um backend implementando trait testavel. + +**Pros:** +- testes de contrato mais diretos; +- menor dependencia do host desktop. + +**Contras:** +- runtime passa a owns detalhes de thread; +- precisa cuidado para nao absorver politica de host/window. + +**Manutenibilidade:** boa se o backend for limpo; ruim se virar acoplamento de host dentro do runtime. + +### Opcao B - Worker thread no host desktop primeiro + +**Abordagem:** implementar o primeiro worker no host winit/pixels e adaptar o runtime a submit/policy. + +**Pros:** +- valida o caso real visual; +- encaixa com surface/present/winit. + +**Contras:** +- mais dificil testar sem janela; +- risco de contrato ficar host-specific; +- portabilidade fica menos comprovada. + +**Manutenibilidade:** boa para desktop, menos boa para hardware proprio. + +### Opcao C - Worker controller runtime + backend host/HAL + +**Abordagem:** criar um controller de worker que implementa handoff, stop token, epoch check e telemetry; o backend concreto vem de host/HAL e pode ser mockado em testes. + +**Pros:** +- separa contrato de backend; +- testavel sem janela; +- mapeia para thread desktop, outro core ou fallback; +- preserva `RenderManager` como coordenador. + +**Contras:** +- desenho inicial mais exigente; +- depende da preparacao da `AGD-0042`. + +**Manutenibilidade:** melhor opcao se queremos worker real sem amarrar a winit. + +## Sugestao / Recomendacao + +Recomendo a **Opcao C - Worker controller runtime + backend host/HAL**. + +O primeiro worker real deveria: + +- consumir um handoff thread-safe single-slot; +- ter stop token e shutdown bounded; +- checar ownership/epoch antes de present; +- expor erros tipados; +- repetir o ultimo frame valido em cadence definida; +- sincronizar telemetry sem alterar semantica da VM; +- ter backend fake/mocked para testes de concorrencia. + +O host desktop pode ser a primeira integracao concreta, mas nao deve ser o unico lugar onde o contrato e testado. + +## Perguntas em Aberto + +- [ ] O worker controller vive em `prometeu-system`, `prometeu-hal`, `prometeu-drivers`, ou host? +- [ ] O handoff thread-safe sera `Mutex>`, canal bounded, atomic slot, ou estrutura propria? +- [ ] Como modelar current work para shutdown e stale owner? +- [ ] Quem chama present: worker, host event loop, ou display cadence service? +- [ ] Como o worker recebe recursos read-only preparados na `AGD-0042`? +- [ ] Qual erro tipado minimo precisamos no primeiro worker? +- [ ] Como provar que a VM nao bloqueia quando o worker atrasa? +- [ ] Qual sera o primeiro backend real: desktop `Gfx`/pixels, framebuffer local, ou fake backend? + +## Criterio para Encerrar + +Esta agenda pode virar decision quando tivermos: + +- local/camada do worker controller; +- estrutura thread-safe de handoff; +- trait/backend de render/present; +- present cadence/repeat; +- shutdown/current work; +- error model; +- test matrix de concorrencia; +- escopo do primeiro worker real. + +## Discussion + +- 2026-06-06: Agenda criada dentro da `DSC-0042` para separar worker real da preparacao de fronteira discutida em `AGD-0042`. + +## Resolution diff --git a/discussion/workflow/decisions/DEC-0032-platform-layer-and-hardwarebridge-elimination.md b/discussion/workflow/decisions/DEC-0032-platform-layer-and-hardwarebridge-elimination.md new file mode 100644 index 00000000..33e2abaf --- /dev/null +++ b/discussion/workflow/decisions/DEC-0032-platform-layer-and-hardwarebridge-elimination.md @@ -0,0 +1,198 @@ +--- +id: DEC-0032 +ticket: real-render-worker-establishment +title: Platform Layer and HardwareBridge Elimination +status: accepted +created: 2026-06-06 +accepted: +agenda: AGD-0042 +plans: [PLN-0098, PLN-0099, PLN-0100, PLN-0101, PLN-0102, PLN-0103, PLN-0104, PLN-0105, PLN-0106, PLN-0107, PLN-0108, PLN-0109] +tags: [runtime, platform, hardware, host, hal, renderer, architecture] +--- + +## Status + +Decision draft emitted from accepted agenda `AGD-0042`. + +This decision is ready for review. Once accepted, it becomes the normative contract for plans that prepare Prometeu's platform layer before the real render worker work in `AGD-0043`. + +## Contexto + +`DEC-0031` established the VM/render boundary: closed render submissions, single-slot handoff, frame pacing, AppMode policy, ownership epoch, and render telemetry. + +The current implementation still depends on `HardwareBridge`, a monolithic trait that exposes render, composer, `Gfx`, audio, input, touch, assets, frame begin, and presentation through one runtime-facing object. That bridge was useful as an early abstraction, but it is no longer sufficient for a high-quality handheld-style runtime. + +The goal is no longer to make `HardwareBridge` the permanent host-implemented interface. The goal is to replace it with an explicit platform layer made of domain services/facades. Runtime code should depend on stable platform contracts; host/board code should provide concrete implementations. + +This decision prepares the platform boundary. It does not implement the real render worker. Worker establishment remains scoped to `AGD-0043`. + +## Decisao + +Prometeu SHALL replace the monolithic `HardwareBridge` model with a platform layer composed of explicit domain services/facades. + +`HardwareBridge` SHALL NOT survive as a final compatibility contract. It MAY exist only as temporary migration scaffold. The end state MUST remove `HardwareBridge` and all production/runtime dependencies on it. + +The migration MAY begin with render, because render blocks the real worker path, but it MUST NOT stop there. The migration MUST cover every domain currently coupled through `HardwareBridge`: render, Game2D frame composition, audio, input/touch, assets/storage, clock/pacing, and telemetry where applicable. + +The first implementation wave SHALL prepare render-worker-safe boundaries without introducing the real worker yet: + +- introduce an owned `RenderSubmissionSink`; +- introduce minimal typed render submission errors; +- remove immediate framebuffer writes from `gfx2d` and `gfxui` syscalls; +- move Game2D frame composition into a logical-side `Game2DFrameComposer` service/facade; +- replace test fixtures centered on `Hardware::new()` with `TestPlatform`. + +## Rationale + +A handheld-quality runtime needs clear separation between platform policy and board/host implementation. + +The runtime/OS owns: + +- lifecycle and AppMode policy; +- VM execution; +- frame pacing; +- render ownership and handoff; +- logical frame composition; +- command buffers; +- resource visibility policy; +- telemetry semantics. + +The host/board implementation owns: + +- window/surface or physical framebuffer; +- concrete present implementation; +- physical input events; +- audio backend; +- IO/storage backend; +- host threads, cores, DMA, or coprocessor mapping. + +A single `HardwareBridge` encourages accidental coupling. It lets VM syscalls, firmware, Hub, renderer, audio, input, assets, and present share the same mutable object. That model makes it too easy for future worker work to pass `&mut Hardware`, `&mut Gfx`, or live `FrameComposer` state across the render boundary. + +Explicit facades make dependencies visible and testable. They also let each domain evolve with the lifecycle, threading, and error semantics it actually needs. + +## Invariantes / Contrato + +### 1. No final `HardwareBridge` + +`HardwareBridge` MUST be removed from the final architecture of this migration. + +During migration, code MAY use adapters or temporary accessors to bridge old and new APIs. Those adapters MUST be treated as implementation scaffolding, not as new public/runtime architecture. + +`HardwareBridge` MUST NOT inherit the new facades as a way to preserve itself as a permanent aggregate contract. + +### 2. Facades live at the right layer + +Contracts/facades SHALL live in `prometeu-hal`. + +Local/default and test implementations SHALL live in `prometeu-drivers` unless a domain has a stronger existing home. + +Operational integration and ownership orchestration SHALL live in `prometeu-system` and host crates. + +### 3. Render submission is owned + +`RenderSubmissionSink` SHALL accept owned `RenderSubmission` values. + +The owned contract is required because asynchronous handoff transfers ownership to a consumer. Local/synchronous paths MAY adapt internally, but the facade MUST be compatible with future worker handoff. + +### 4. Render submission errors are typed + +The preparation work SHALL introduce a minimal typed error surface for submission/render publication. + +The API MUST NOT be born infallible if the next worker stage will immediately need typed failure. Panic containment may remain as a safety net, but normal backend failure should have an explicit result type. + +### 5. `gfx2d` and `gfxui` syscalls buffer commands only + +`gfx2d.*` and `gfxui.*` syscalls MUST NOT mutate the framebuffer immediately. + +Those syscalls SHALL record commands into the appropriate logical command buffer. Pixels change only when a closed render submission is consumed/published by the render path. + +The first regression tests MUST include `gfx2d.clear` and `gfx2d.draw_text` proving that syscalls buffer commands and presentation happens only after frame closure/publication. + +### 6. Game2D composition is logical-side state + +`bind_scene`, `set_camera`, `emit_sprite`, and `close_game2d_packet` are logical frame preparation, not physical backend behavior. + +The migration SHALL introduce `Game2DFrameComposer` as the logical-side service/facade for Game2D frame composition. Its implementation may move in phases, but it MUST NOT remain conceptually part of a physical render backend. + +### 7. Tests use `TestPlatform` + +Tests that need a full local platform fixture SHALL migrate toward `TestPlatform`. + +`TestPlatform` SHOULD keep tests ergonomic while making dependencies explicit through the new facades. It MUST NOT preserve `HardwareBridge` as hidden compatibility. + +### 8. Render may migrate first, but the migration is complete only when every domain is moved + +The render path MAY be migrated first. + +The architecture is not complete until audio, input/touch, assets/storage, clock/pacing, telemetry where applicable, firmware, Hub, runtime, host desktop, and tests no longer depend on `HardwareBridge`. + +## Impactos + +### Spec + +Specs need to describe the platform layer as explicit services/facades rather than a monolithic hardware bridge. + +Render specifications must state that `gfx2d`/`gfxui` syscalls buffer commands and that framebuffer mutation occurs through render submission consumption/publication. + +### Runtime + +Runtime code must stop depending on `&mut dyn HardwareBridge`. + +VM dispatch must stop using `hw.gfx_mut()` for immediate drawing. Runtime frame closure must depend on logical services such as `Game2DFrameComposer` and publication facades such as `RenderSubmissionSink`. + +### HAL + +`prometeu-hal` must define the stable contracts for the platform layer. + +At minimum, the first wave needs `RenderSubmissionSink` and its typed error. Later waves should define or refine composer, audio, input/touch, assets/storage, clock/pacing, and telemetry facades. + +### Drivers + +`prometeu-drivers` should provide local/default implementations and `TestPlatform`. + +The existing `prometeu_drivers::Hardware` may be used as a migration source, but it must not remain the final runtime-facing architecture. + +### Host + +Host crates must move from directly owning and exposing `Hardware` as the runtime's universal bridge toward implementing platform services. + +The desktop host may remain the first concrete integration, but the contracts must remain portable to handheld board support, emulator, web, or coprocessor/DMA implementations. + +### Firmware / Hub + +Firmware and Hub must migrate away from `publish_render_submission` on `HardwareBridge` and toward explicit render submission publication/platform services. + +### Tests + +Tests must migrate from direct `Hardware::new()` dependence to `TestPlatform` or narrower facade-specific fixtures. + +Tests must be updated where they assume immediate framebuffer mutation from `gfx2d`/`gfxui` syscalls. + +## Referencias + +- `AGD-0042`: Host Hardware and Render Boundary Preparation. +- `AGD-0043`: Real Render Worker Establishment. +- `DEC-0031`: VM and Render Parallel Execution Boundary. +- `LSN-0048`: Render Workers Need a Closed Packet Contract. + +## Propagacao Necessaria + +Create implementation plans before editing specs or code. + +Recommended plan sequence: + +1. introduce `RenderSubmissionSink` and minimal typed submit error; +2. migrate local render publication away from `HardwareBridge`; +3. remove immediate framebuffer writes from `gfx2d`/`gfxui` syscalls and update tests; +4. introduce `Game2DFrameComposer` as logical-side service/facade; +5. introduce `RuntimePlatform`/`Platform` and `TestPlatform`; +6. migrate firmware, Hub, runtime, host desktop, and tests to explicit facades; +7. migrate remaining domains: audio, input/touch, assets/storage, clock/pacing, telemetry as needed; +8. remove `HardwareBridge`; +9. update specs and lessons after implementation publishes the new structure. + +`AGD-0043` SHOULD NOT move to implementation until this decision has produced enough platform boundary work that a render worker can be built without `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state crossing into the worker. + +## Revision Log + +- 2026-06-06: Initial decision draft generated from accepted `AGD-0042`. diff --git a/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md b/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md new file mode 100644 index 00000000..2ec7f397 --- /dev/null +++ b/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md @@ -0,0 +1,81 @@ +--- +id: PLN-0098 +ticket: real-render-worker-establishment +title: Define Platform Facade Contracts +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [runtime, platform, hal, facades] +--- + +## Objective + +Define the first `prometeu-hal` platform facade contracts that will replace the monolithic `HardwareBridge` model. + +## Background + +`DEC-0032` requires Prometeu to move from one runtime-facing `HardwareBridge` to explicit platform services/facades. This first plan creates the stable contract surface without migrating callers yet. + +## Scope + +### Included + +- Add facade modules/types in `crates/console/prometeu-hal`. +- Define names and module ownership for `RuntimePlatform`, `RenderSubmissionSink`, `RenderBackend`, `Game2DFrameComposer`, and `TestPlatform` integration points. +- Add minimal documentation explaining that `HardwareBridge` is legacy migration scaffold. + +### Excluded + +- Do not remove `HardwareBridge`. +- Do not migrate syscalls, firmware, Hub, host, or tests yet. +- Do not implement the real render worker. + +## Execution Steps + +### Step 1 - Add platform facade module + +**What:** Create a `platform` module in `prometeu-hal`. +**How:** Add facade trait declarations and re-exports without changing existing call sites. +**File(s):** `crates/console/prometeu-hal/src/platform.rs`, `crates/console/prometeu-hal/src/lib.rs`. + +### Step 2 - Declare domain facade boundaries + +**What:** Define initial placeholder traits for render submission, render backend, Game2D composer, audio, input/touch, assets/storage, clock/pacing, and telemetry. +**How:** Keep non-render traits minimal or marker-like when detailed APIs are not yet migrated. +**File(s):** `crates/console/prometeu-hal/src/platform.rs`. + +### Step 3 - Document migration status + +**What:** Mark `HardwareBridge` as legacy scaffold. +**How:** Add module-level comments stating it must be removed by later plans. +**File(s):** `crates/console/prometeu-hal/src/hardware_bridge.rs`. + +## Test Requirements + +### Unit Tests + +- Compile-only tests or trait object assertions where useful. + +### Integration Tests + +- Existing HAL/system/driver tests must continue to pass. + +### Manual Verification + +- Confirm `HardwareBridge` has no new dependencies on the platform facades. + +## Acceptance Criteria + +- [ ] Platform facade module exists in `prometeu-hal`. +- [ ] `HardwareBridge` is explicitly documented as migration scaffold. +- [ ] No runtime behavior changes. +- [ ] Existing tests pass. + +## Dependencies + +- `DEC-0032`. + +## Risks + +- Introducing too much API too early can freeze poor names. Keep this plan narrow. diff --git a/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md b/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md new file mode 100644 index 00000000..844568be --- /dev/null +++ b/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md @@ -0,0 +1,82 @@ +--- +id: PLN-0099 +ticket: real-render-worker-establishment +title: Introduce Owned RenderSubmissionSink +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [runtime, renderer, hal, errors] +--- + +## Objective + +Introduce an owned `RenderSubmissionSink` facade with minimal typed submission errors. + +## Background + +`DEC-0032` requires render publication to move away from `HardwareBridge` and to accept owned `RenderSubmission` values so the contract is compatible with async handoff. + +## Scope + +### Included + +- Define `RenderSubmissionSink`. +- Define minimal `RenderSubmitError`. +- Implement the facade for the current local driver path. + +### Excluded + +- Do not migrate all publication call sites yet. +- Do not spawn a render worker. +- Do not remove `HardwareBridge::publish_render_submission`. + +## Execution Steps + +### Step 1 - Define sink and error + +**What:** Add `RenderSubmissionSink` and `RenderSubmitError`. +**How:** `submit_render_submission(&mut self, submission: RenderSubmission) -> Result<(), RenderSubmitError>`. +**File(s):** `crates/console/prometeu-hal/src/platform.rs` or a render-specific HAL module. + +### Step 2 - Implement local sink + +**What:** Implement the sink for the current local driver implementation. +**How:** Adapt owned submission to the existing local `Hardware` publication path. +**File(s):** `crates/console/prometeu-drivers/src/hardware.rs`. + +### Step 3 - Add tests + +**What:** Prove owned submission is consumed by the local sink. +**How:** Add driver test using `RenderSubmission::game2d` and `RenderSubmission::shell_ui`. +**File(s):** `crates/console/prometeu-drivers/src/hardware.rs` or platform test module. + +## Test Requirements + +### Unit Tests + +- Local sink accepts owned Game2D and ShellUi submissions. +- Error type is constructible and debuggable. + +### Integration Tests + +- `cargo test -p prometeu-hal -p prometeu-drivers`. + +### Manual Verification + +- Confirm the sink does not take `&RenderSubmission`. + +## Acceptance Criteria + +- [ ] `RenderSubmissionSink` takes owned `RenderSubmission`. +- [ ] Minimal typed error exists. +- [ ] Local driver implements the sink. +- [ ] Existing publication behavior remains intact. + +## Dependencies + +- `PLN-0098`. + +## Risks + +- Local paths may still need temporary borrow adaptation. Keep it internal to the implementation. diff --git a/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md b/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md new file mode 100644 index 00000000..670963e4 --- /dev/null +++ b/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md @@ -0,0 +1,80 @@ +--- +id: PLN-0100 +ticket: real-render-worker-establishment +title: Migrate Local Render Publication to RenderSubmissionSink +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [runtime, renderer, platform] +--- + +## Objective + +Move local render publication call sites from `HardwareBridge::publish_render_submission` to `RenderSubmissionSink`. + +## Background + +The sink must become the publication boundary before a real worker can replace the local path. Runtime, firmware, and Hub currently publish through the monolithic bridge. + +## Scope + +### Included + +- Runtime local render surface uses `RenderSubmissionSink`. +- Firmware splash/crash publication uses the sink. +- Hub Shell UI publication uses the sink or a temporary adapter. + +### Excluded + +- Do not remove `HardwareBridge`. +- Do not migrate non-render domains. +- Do not change command buffering semantics yet. + +## Execution Steps + +### Step 1 - Adapt runtime tick publication + +**What:** Replace `HardwareRenderSurface` dependency on `HardwareBridge::publish_render_submission`. +**How:** Route submission through `RenderSubmissionSink`. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tick.rs`. + +### Step 2 - Adapt firmware and Hub publication + +**What:** Stop direct calls to `hw.publish_render_submission`. +**How:** Use the sink through explicit platform/render facade access or temporary adapter. +**File(s):** `crates/console/prometeu-firmware/src/firmware_step_splash_screen.rs`, `crates/console/prometeu-firmware/src/firmware_step_crash_screen.rs`, `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`. + +### Step 3 - Update tests + +**What:** Adjust tests that call `HardwareBridge::publish_render_submission`. +**How:** Use `RenderSubmissionSink` where render publication is the target. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tests.rs`, driver tests. + +## Test Requirements + +### Unit Tests + +- Existing render manager tests pass. + +### Integration Tests + +- `cargo test -p prometeu-system -p prometeu-firmware -p prometeu-drivers`. + +### Manual Verification + +- `rg "publish_render_submission" crates/console` shows only legacy implementation or intentionally unmigrated scaffold. + +## Acceptance Criteria + +- [ ] Runtime publication uses `RenderSubmissionSink`. +- [ ] Firmware/Hub publication no longer depend directly on the monolithic bridge. +- [ ] Tests pass. + +## Dependencies + +- `PLN-0099`. + +## Risks + +- Firmware code may need a small context shape change. Keep compatibility adapters temporary and explicit. diff --git a/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md b/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md new file mode 100644 index 00000000..69975865 --- /dev/null +++ b/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md @@ -0,0 +1,82 @@ +--- +id: PLN-0101 +ticket: real-render-worker-establishment +title: Remove Immediate Gfx2D and GfxUI Syscall Rendering +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [runtime, gfx, syscalls, renderer] +--- + +## Objective + +Make `gfx2d.*` and `gfxui.*` syscalls buffer commands only, without mutating the framebuffer immediately. + +## Background + +`DEC-0032` requires framebuffer mutation to occur through render submission consumption/publication, not inside VM syscall dispatch. This is required before render can run independently. + +## Scope + +### Included + +- Remove `hw.gfx_mut().*` immediate draw calls from VM dispatch for `gfx2d` and `gfxui`. +- Add regression tests for `gfx2d.clear` and `gfx2d.draw_text`. +- Update tests that assumed immediate pixel mutation. + +### Excluded + +- Do not remove `GfxBridge`. +- Do not migrate composer in this plan. +- Do not implement worker. + +## Execution Steps + +### Step 1 - Remove immediate Game gfx writes + +**What:** Delete direct `hw.gfx_mut()` rendering in `Gfx*` syscall handlers. +**How:** Keep command buffering into `gfx2d_commands`. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. + +### Step 2 - Remove immediate Shell UI writes + +**What:** Delete direct `hw.gfx_mut()` rendering in `GfxUi*` syscall handlers. +**How:** Keep command buffering into `gfxui_commands`. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. + +### Step 3 - Add regression tests + +**What:** Prove syscalls do not affect pixels until publication. +**How:** Tests for `gfx2d.clear` and `gfx2d.draw_text` should inspect command buffers and frame publication. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tests.rs`. + +## Test Requirements + +### Unit Tests + +- `gfx2d.clear` buffers only. +- `gfx2d.draw_text` buffers only. +- Shell UI commands buffer only. + +### Integration Tests + +- Stress cartridge still renders after frame publication. + +### Manual Verification + +- `rg "gfx_mut\\(\\).*draw|gfx_mut\\(\\).*clear|gfx_mut\\(\\).*fill" dispatch.rs` returns no immediate render calls. + +## Acceptance Criteria + +- [ ] Gfx syscalls no longer mutate framebuffer immediately. +- [ ] Pixels change only after render submission publication. +- [ ] Tests updated to the new contract. + +## Dependencies + +- `PLN-0100`. + +## Risks + +- Debug workflows may have relied on immediate drawing. Tests must make the new frame boundary explicit. diff --git a/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md b/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md new file mode 100644 index 00000000..0f715ca9 --- /dev/null +++ b/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md @@ -0,0 +1,82 @@ +--- +id: PLN-0102 +ticket: real-render-worker-establishment +title: Introduce Game2DFrameComposer Logical Service +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [runtime, composer, game2d, platform] +--- + +## Objective + +Introduce `Game2DFrameComposer` as the logical-side service/facade for Game2D frame preparation. + +## Background + +`DEC-0032` states that `bind_scene`, `set_camera`, `emit_sprite`, and `close_game2d_packet` are logical frame preparation, not physical backend behavior. + +## Scope + +### Included + +- Define a `Game2DFrameComposer` facade. +- Move or wrap existing `FrameComposer` behavior under logical-side naming. +- Migrate composer syscalls to use the facade rather than generic hardware. + +### Excluded + +- Do not rewrite scene cache internals. +- Do not move viewport cache across threads. +- Do not implement worker. + +## Execution Steps + +### Step 1 - Define composer facade + +**What:** Add `Game2DFrameComposer` trait or service contract. +**How:** Include `begin_frame`, `bind_scene`, `unbind_scene`, `set_camera`, `emit_sprite`, `close_game2d_packet`, and required read checks. +**File(s):** `crates/console/prometeu-hal/src/platform.rs` or composer-specific HAL module. + +### Step 2 - Implement using current composer + +**What:** Make current local driver composer satisfy the new logical service. +**How:** Reuse `FrameComposer` implementation under adapter/implementation. +**File(s):** `crates/console/prometeu-drivers/src/frame_composer.rs`, `crates/console/prometeu-drivers/src/hardware.rs`. + +### Step 3 - Migrate composer syscalls + +**What:** Dispatch composer syscalls through `Game2DFrameComposer`. +**How:** Remove dependency on `HardwareBridge` methods for composer. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. + +## Test Requirements + +### Unit Tests + +- Composer facade closes packets equivalent to previous `FrameComposer`. +- Invalid scene/bank behavior remains unchanged. + +### Integration Tests + +- Existing scene/composer runtime tests pass. + +### Manual Verification + +- Composer syscalls no longer require `&mut dyn HardwareBridge`. + +## Acceptance Criteria + +- [ ] `Game2DFrameComposer` exists as logical service/facade. +- [ ] Composer syscalls route through it. +- [ ] Existing Game2D scene rendering remains correct. + +## Dependencies + +- `PLN-0098`. +- `PLN-0101`. + +## Risks + +- Scene cache ownership can be accidentally moved too early. Keep cache behavior local while changing the boundary. diff --git a/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md b/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md new file mode 100644 index 00000000..ee18af08 --- /dev/null +++ b/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md @@ -0,0 +1,82 @@ +--- +id: PLN-0103 +ticket: real-render-worker-establishment +title: Introduce RuntimePlatform and TestPlatform +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [runtime, platform, tests] +--- + +## Objective + +Introduce `RuntimePlatform`/`Platform` as the aggregate of explicit platform services, and `TestPlatform` as the default test fixture. + +## Background + +`DEC-0032` requires tests and runtime integration to depend on explicit facades rather than `Hardware::new()` and `HardwareBridge`. + +## Scope + +### Included + +- Define aggregate platform access shape. +- Add `TestPlatform` in `prometeu-drivers`. +- Start migrating representative VM runtime tests. + +### Excluded + +- Do not migrate all tests in this plan. +- Do not remove `HardwareBridge`. +- Do not migrate all host code. + +## Execution Steps + +### Step 1 - Define platform aggregate + +**What:** Add `RuntimePlatform` or `Platform` aggregate facade. +**How:** Expose accessors for render sink, Game2D composer, audio, input/touch, assets/storage, clock/pacing, and telemetry placeholders. +**File(s):** `crates/console/prometeu-hal/src/platform.rs`. + +### Step 2 - Add TestPlatform + +**What:** Provide ergonomic test fixture implementing the initial platform facades. +**How:** Reuse current local driver components internally without exposing `HardwareBridge`. +**File(s):** `crates/console/prometeu-drivers/src/test_platform.rs`, `crates/console/prometeu-drivers/src/lib.rs`. + +### Step 3 - Migrate representative tests + +**What:** Move a small set of runtime tests from `Hardware::new()` to `TestPlatform`. +**How:** Choose tests covering render, composer, and asset surfaces. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tests.rs`, asset/memcard test modules as appropriate. + +## Test Requirements + +### Unit Tests + +- `TestPlatform` initializes all required facades. + +### Integration Tests + +- Migrated runtime tests pass with `TestPlatform`. + +### Manual Verification + +- New tests do not import `prometeu_drivers::Hardware` directly. + +## Acceptance Criteria + +- [ ] `RuntimePlatform`/`Platform` aggregate exists. +- [ ] `TestPlatform` exists and is used by representative tests. +- [ ] Test ergonomics remain acceptable. + +## Dependencies + +- `PLN-0098`. +- `PLN-0099`. +- `PLN-0102`. + +## Risks + +- Overly broad aggregate can recreate `HardwareBridge`. Keep domain access explicit and named. diff --git a/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md b/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md new file mode 100644 index 00000000..1b00bc5a --- /dev/null +++ b/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md @@ -0,0 +1,79 @@ +--- +id: PLN-0104 +ticket: real-render-worker-establishment +title: Migrate VM Runtime HostContext to Platform Services +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [runtime, vm, platform, hostcontext] +--- + +## Objective + +Change VM runtime execution from receiving `&mut dyn HardwareBridge` to receiving explicit platform services. + +## Background + +`HostContext` currently carries `Option<&mut dyn HardwareBridge>`. That shape lets every syscall see the entire bridge. `DEC-0032` requires explicit services instead. + +## Scope + +### Included + +- Introduce a platform-aware `HostContext` shape. +- Migrate VM runtime tick/dispatch to use platform services. +- Preserve VM tests and fault behavior. + +### Excluded + +- Do not migrate firmware/Hub in this plan. +- Do not remove `HardwareBridge` yet. + +## Execution Steps + +### Step 1 - Add platform host context + +**What:** Extend or replace `HostContext` with platform-service access. +**How:** Add methods for required VM syscall domains instead of `require_hw()`. +**File(s):** `crates/console/prometeu-hal/src/host_context.rs`. + +### Step 2 - Migrate runtime tick + +**What:** Tick uses platform services for begin-frame, composer frame closure, render sink, audio clear, and telemetry reads. +**How:** Replace direct bridge access with explicit facade calls. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tick.rs`. + +### Step 3 - Migrate dispatch + +**What:** VM syscall dispatch uses explicit facade methods. +**How:** Remove `ctx.require_hw()` as the central dispatch dependency. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. + +## Test Requirements + +### Unit Tests + +- VM dispatch tests pass with platform context. + +### Integration Tests + +- `cargo test -p prometeu-system`. + +### Manual Verification + +- `rg "require_hw|HardwareBridge" vm_runtime` shows no production VM runtime dependency except temporary compatibility adapters explicitly marked. + +## Acceptance Criteria + +- [ ] VM runtime dispatch no longer depends on the monolithic bridge. +- [ ] Tick uses explicit platform services. +- [ ] Existing VM fault behavior remains unchanged. + +## Dependencies + +- `PLN-0103`. + +## Risks + +- `HostContext` is used in VM unit tests. Migration must keep no-hardware test contexts ergonomic. diff --git a/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md b/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md new file mode 100644 index 00000000..a7165b5e --- /dev/null +++ b/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md @@ -0,0 +1,81 @@ +--- +id: PLN-0105 +ticket: real-render-worker-establishment +title: Migrate Firmware and Hub to Platform Services +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [firmware, hub, platform, render] +--- + +## Objective + +Move firmware screens and Hub UI away from `HardwareBridge` and onto explicit platform services. + +## Background + +Firmware splash/crash and Hub currently receive `&mut dyn HardwareBridge` and publish Shell UI directly through it. `DEC-0032` requires migration to platform facades. + +## Scope + +### Included + +- Firmware context uses platform services. +- Splash/crash screens publish via render sink. +- Hub input/render uses input/render facades. + +### Excluded + +- Do not implement worker. +- Do not change Hub UX or firmware state machine semantics. + +## Execution Steps + +### Step 1 - Update firmware context + +**What:** Replace `PrometeuContext` hardware bridge field with platform services. +**How:** Expose only the services firmware needs: input, assets, render sink. +**File(s):** `crates/console/prometeu-firmware/src/firmware/prometeu_context.rs`, `firmware.rs`. + +### Step 2 - Migrate firmware screens + +**What:** Splash and crash screens publish through `RenderSubmissionSink`. +**How:** Build Shell UI packet and submit owned submission. +**File(s):** `crates/console/prometeu-firmware/src/firmware_step_splash_screen.rs`, `firmware_step_crash_screen.rs`. + +### Step 3 - Migrate Hub + +**What:** Hub uses platform input/render services instead of `HardwareBridge`. +**How:** Replace activation input and render publication dependencies. +**File(s):** `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`. + +## Test Requirements + +### Unit Tests + +- Firmware state tests pass. +- Hub UI tests pass. + +### Integration Tests + +- `cargo test -p prometeu-firmware -p prometeu-system`. + +### Manual Verification + +- Firmware/Hub no longer call `publish_render_submission` on `HardwareBridge`. + +## Acceptance Criteria + +- [ ] Firmware context no longer exposes the monolithic bridge. +- [ ] Splash/crash/Hub render via render sink. +- [ ] Hub input uses platform/input facade. + +## Dependencies + +- `PLN-0103`. +- `PLN-0104`. + +## Risks + +- Firmware code is lifecycle-sensitive. Keep behavior changes out of scope. diff --git a/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md b/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md new file mode 100644 index 00000000..016ccd71 --- /dev/null +++ b/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md @@ -0,0 +1,79 @@ +--- +id: PLN-0106 +ticket: real-render-worker-establishment +title: Migrate Desktop Host to Platform Services +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [host, desktop, platform] +--- + +## Objective + +Move the desktop host from directly exposing `prometeu_drivers::Hardware` as the runtime bridge to owning a platform implementation. + +## Background + +The host currently creates `Hardware::new()` and passes it into firmware/runtime paths. `DEC-0032` requires hosts to implement platform services, not to expose a monolithic bridge. + +## Scope + +### Included + +- Introduce desktop/local platform ownership in the host. +- Replace host usage of `Hardware::W/H` with platform display constants or framebuffer descriptor. +- Keep the same visual/audio/input behavior. + +### Excluded + +- Do not implement render worker thread. +- Do not change windowing library or presentation policy. + +## Execution Steps + +### Step 1 - Add host platform field + +**What:** Host runner owns a platform implementation rather than raw `Hardware`. +**How:** Wrap current local driver implementation behind platform facades. +**File(s):** `crates/host/prometeu-host-desktop-winit/src/runner.rs`. + +### Step 2 - Replace direct hardware constants + +**What:** Stop reading `Hardware::W/H` directly in host rendering/input. +**How:** Use a framebuffer/display descriptor exposed by platform/render service. +**File(s):** `crates/host/prometeu-host-desktop-winit/src/runner.rs`, `input.rs`. + +### Step 3 - Update debugger host setup + +**What:** Debugger paths instantiate platform fixture/implementation. +**How:** Replace `Hardware::new()` in debugger setup. +**File(s):** `crates/host/prometeu-host-desktop-winit/src/debugger.rs`. + +## Test Requirements + +### Unit Tests + +- Host crate tests compile. + +### Integration Tests + +- `cargo test -p prometeu-host-desktop-winit`. + +### Manual Verification + +- Desktop host still runs stress cartridge. + +## Acceptance Criteria + +- [ ] Desktop host owns platform services. +- [ ] Host no longer treats `Hardware` as runtime bridge. +- [ ] Display dimensions come from platform/render descriptor. + +## Dependencies + +- `PLN-0105`. + +## Risks + +- Desktop host mixes runtime and window concerns. Keep worker threading out of this plan. diff --git a/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md b/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md new file mode 100644 index 00000000..e7f13106 --- /dev/null +++ b/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md @@ -0,0 +1,89 @@ +--- +id: PLN-0107 +ticket: real-render-worker-establishment +title: Migrate Remaining Platform Domains +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [platform, audio, input, assets, clock, telemetry] +--- + +## Objective + +Migrate non-render domains out of `HardwareBridge` into explicit platform services. + +## Background + +`DEC-0032` permits render to migrate first but requires the full migration to cover audio, input/touch, assets/storage, clock/pacing, and telemetry where applicable. + +## Scope + +### Included + +- Audio service/facade migration. +- Input/touch service/facade migration. +- Assets/storage service/facade migration. +- Clock/pacing and telemetry surface migration where currently coupled. + +### Excluded + +- Do not change domain semantics. +- Do not implement async IO unless already supported by existing services. + +## Execution Steps + +### Step 1 - Migrate audio + +**What:** Replace `hw.audio()`/`hw.audio_mut()` production dependencies. +**How:** Route through explicit audio platform service. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`, firmware/host integration files. + +### Step 2 - Migrate input and touch + +**What:** Replace `hw.pad()`/`hw.touch()` dependencies. +**How:** Expose input snapshots through platform input service. +**File(s):** `crates/console/prometeu-system`, `crates/console/prometeu-firmware`, host input modules. + +### Step 3 - Migrate assets/storage + +**What:** Replace `hw.assets()`/`hw.assets_mut()` dependencies. +**How:** Route through asset/storage services while preserving commit and bank visibility semantics. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime`, `crates/console/prometeu-firmware`. + +### Step 4 - Migrate clock/pacing/telemetry coupling + +**What:** Move any platform-facing clock or telemetry coupling out of `HardwareBridge`. +**How:** Use explicit runtime/platform services. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime`, HAL telemetry modules. + +## Test Requirements + +### Unit Tests + +- Audio/input/asset syscall tests pass. + +### Integration Tests + +- `cargo test -p prometeu-system -p prometeu-firmware -p prometeu-drivers`. + +### Manual Verification + +- `rg "audio_mut|pad_mut|touch_mut|assets_mut" crates/console` does not show production bridge dependencies. + +## Acceptance Criteria + +- [ ] Audio migrated to platform service. +- [ ] Input/touch migrated to platform service. +- [ ] Assets/storage migrated to platform service. +- [ ] Clock/pacing/telemetry bridge coupling removed where applicable. + +## Dependencies + +- `PLN-0104`. +- `PLN-0105`. +- `PLN-0106`. + +## Risks + +- Asset visibility semantics are high risk. Keep behavior-preserving tests broad. diff --git a/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md b/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md new file mode 100644 index 00000000..cd08c9eb --- /dev/null +++ b/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md @@ -0,0 +1,81 @@ +--- +id: PLN-0108 +ticket: real-render-worker-establishment +title: Migrate Tests to TestPlatform +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [tests, platform, fixtures] +--- + +## Objective + +Replace remaining test dependence on `Hardware::new()` and `HardwareBridge` with `TestPlatform` or narrower facade fixtures. + +## Background + +`DEC-0032` requires tests that need a full local platform fixture to use `TestPlatform`, not hidden `HardwareBridge` compatibility. + +## Scope + +### Included + +- VM runtime tests. +- Firmware tests. +- Driver integration tests that still use monolithic hardware only as fixture. +- Host debugger tests where applicable. + +### Excluded + +- Do not remove production bridge until tests are migrated. +- Do not change behavior under test. + +## Execution Steps + +### Step 1 - Inventory test dependencies + +**What:** Find all direct `Hardware::new()` and `HardwareBridge` test uses. +**How:** Use `rg` and classify by domain. +**File(s):** test modules across `crates/console` and `crates/host`. + +### Step 2 - Migrate full-platform tests + +**What:** Replace `Hardware::new()` with `TestPlatform`. +**How:** Use explicit facade accessors in assertions. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tests*.rs`, firmware tests. + +### Step 3 - Migrate narrow tests + +**What:** Replace full platform with narrower fixtures where only one domain is under test. +**How:** Use render/composer/audio/input/asset fixtures directly. +**File(s):** driver and runtime test modules. + +## Test Requirements + +### Unit Tests + +- All migrated tests pass. + +### Integration Tests + +- `cargo test --workspace` or relevant workspace subset if full workspace is too broad. + +### Manual Verification + +- `rg "Hardware::new\\(|HardwareBridge" crates --glob '*test*'` shows no test dependency except tests explicitly covering legacy removal. + +## Acceptance Criteria + +- [ ] `TestPlatform` is the default full platform fixture. +- [ ] Tests no longer preserve `HardwareBridge` by habit. +- [ ] Narrow tests use narrower fixtures. + +## Dependencies + +- `PLN-0103`. +- `PLN-0107`. + +## Risks + +- Large mechanical changes can hide behavior regressions. Keep commits grouped by test domain. diff --git a/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md b/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md new file mode 100644 index 00000000..f795d8c0 --- /dev/null +++ b/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md @@ -0,0 +1,91 @@ +--- +id: PLN-0109 +ticket: real-render-worker-establishment +title: Remove HardwareBridge and Update Specs +status: open +created: 2026-06-06 +completed: +ref_decisions: [DEC-0032] +tags: [platform, cleanup, specs, hal] +--- + +## Objective + +Remove `HardwareBridge` from the codebase and publish the new platform layer contract in specs. + +## Background + +`DEC-0032` explicitly forbids preserving `HardwareBridge` as final compatibility. After all domains and tests migrate, the bridge must be deleted. + +## Scope + +### Included + +- Delete `HardwareBridge`. +- Remove exports/imports and adapters that only preserve bridge compatibility. +- Update canonical specs in English. +- Run validation and broad tests. + +### Excluded + +- Do not implement the real render worker. +- Do not change platform service semantics beyond cleanup. + +## Execution Steps + +### Step 1 - Delete bridge + +**What:** Remove `HardwareBridge` trait and module. +**How:** Delete source file and remove exports/imports. +**File(s):** `crates/console/prometeu-hal/src/hardware_bridge.rs`, `crates/console/prometeu-hal/src/lib.rs`, all remaining imports. + +### Step 2 - Remove compatibility adapters + +**What:** Delete migration-only adapters. +**How:** Replace final references with direct platform services or remove dead code. +**File(s):** HAL, drivers, system, firmware, host modules touched by previous plans. + +### Step 3 - Update specs + +**What:** Document platform layer and removal of monolithic hardware bridge. +**How:** Update runtime/host/render specs in English. +**File(s):** `docs/specs/runtime/*.md` and related canonical specs. + +### Step 4 - Validate and test + +**What:** Run discussion validation and broad test suite. +**How:** Use `discussion validate` and cargo tests for affected crates. +**File(s):** repository root. + +## Test Requirements + +### Unit Tests + +- HAL, drivers, firmware, system tests pass. + +### Integration Tests + +- Host desktop crate compiles/tests. +- Stress cartridge still runs through local platform path. + +### Manual Verification + +- `rg "HardwareBridge"` returns no production references. +- `rg "Hardware::new\\("` returns no runtime-facing dependency; any remaining use is local implementation detail or removed. + +## Acceptance Criteria + +- [ ] `HardwareBridge` is deleted. +- [ ] No production/runtime dependencies remain. +- [ ] Specs describe platform services/facades. +- [ ] Tests pass. +- [ ] `AGD-0043` can proceed without needing `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state in the worker. + +## Dependencies + +- `PLN-0107`. +- `PLN-0108`. + +## Risks + +- Removing the bridge too early will cause broad breakage. Only execute after dependency searches show all call sites have migrated. From 9b03d13a0a54cb93c28966a0be879fc9ba5fa959 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 05:43:27 +0100 Subject: [PATCH 17/47] implements PLN-0098 --- .../prometeu-hal/src/hardware_bridge.rs | 5 + crates/console/prometeu-hal/src/lib.rs | 6 + crates/console/prometeu-hal/src/platform.rs | 136 ++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 crates/console/prometeu-hal/src/platform.rs diff --git a/crates/console/prometeu-hal/src/hardware_bridge.rs b/crates/console/prometeu-hal/src/hardware_bridge.rs index fc06d80c..6b39c911 100644 --- a/crates/console/prometeu-hal/src/hardware_bridge.rs +++ b/crates/console/prometeu-hal/src/hardware_bridge.rs @@ -6,6 +6,11 @@ use crate::render_submission::{Game2DFramePacket, RenderSubmission}; use crate::sprite::Sprite; use crate::touch_bridge::TouchBridge; +/// Legacy migration scaffold for code that has not moved to `platform` facades yet. +/// +/// `HardwareBridge` is intentionally not the final runtime-facing contract. New +/// platform boundaries belong in `crate::platform`, and this trait must be +/// removed after runtime, firmware, host, and tests finish migrating. pub trait HardwareBridge { fn begin_frame(&mut self); fn bind_scene(&mut self, scene_bank_id: usize) -> bool; diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index fdd14e47..70067fa1 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -19,6 +19,7 @@ pub mod log; pub mod native_helpers; pub mod native_interface; pub mod pad_bridge; +pub mod platform; pub mod primitives; pub mod render_submission; pub mod sample; @@ -46,6 +47,11 @@ pub use input_signals::InputSignals; pub use native_helpers::{expect_bool, expect_int}; pub use native_interface::{NativeInterface, SyscallId}; pub use pad_bridge::PadBridge; +pub use platform::{ + AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform, + RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform, TelemetryPlatform, + TestPlatform, +}; pub use render_submission::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket, Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderOwnership, RenderSubmission, diff --git a/crates/console/prometeu-hal/src/platform.rs b/crates/console/prometeu-hal/src/platform.rs new file mode 100644 index 00000000..0cd8d3fc --- /dev/null +++ b/crates/console/prometeu-hal/src/platform.rs @@ -0,0 +1,136 @@ +use crate::asset_bridge::AssetBridge; +use crate::audio_bridge::AudioBridge; +use crate::composer_status::ComposerOpStatus; +use crate::pad_bridge::PadBridge; +use crate::render_submission::{Game2DFramePacket, RenderSubmission}; +use crate::sprite::Sprite; +use crate::telemetry::TelemetryFrame; +use crate::touch_bridge::TouchBridge; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderSubmitError { + BackendUnavailable, + SubmissionRejected, +} + +pub trait RenderSubmissionSink { + fn submit_render_submission( + &mut self, + submission: RenderSubmission, + ) -> Result<(), RenderSubmitError>; +} + +pub trait RenderBackend { + fn render_frame(&mut self); +} + +pub trait Game2DFrameComposer { + fn begin_frame(&mut self); + fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus; + fn unbind_scene(&mut self); + fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus; + fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus; + fn close_game2d_packet(&self) -> Game2DFramePacket; + fn has_glyph_bank(&self, bank_id: usize) -> bool; +} + +pub trait AudioPlatform: AudioBridge {} +impl AudioPlatform for T {} + +pub trait InputPlatform { + fn pad(&self) -> &dyn PadBridge; + fn pad_mut(&mut self) -> &mut dyn PadBridge; + fn touch(&self) -> &dyn TouchBridge; + fn touch_mut(&mut self) -> &mut dyn TouchBridge; +} + +pub trait AssetStoragePlatform: AssetBridge {} +impl AssetStoragePlatform for T {} + +pub trait ClockPacingPlatform {} + +pub trait TelemetryPlatform { + fn telemetry_snapshot(&self) -> Option { + None + } +} + +pub trait RuntimePlatform { + fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink; + fn render_backend(&mut self) -> &mut dyn RenderBackend; + fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer; + fn audio(&self) -> &dyn AudioPlatform; + fn audio_mut(&mut self) -> &mut dyn AudioPlatform; + fn input(&self) -> &dyn InputPlatform; + fn input_mut(&mut self) -> &mut dyn InputPlatform; + fn assets(&self) -> &dyn AssetStoragePlatform; + fn assets_mut(&mut self) -> &mut dyn AssetStoragePlatform; + fn telemetry(&self) -> &dyn TelemetryPlatform; +} + +pub trait TestPlatform: RuntimePlatform {} +impl TestPlatform for T {} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_render_sink_object(_: &mut dyn RenderSubmissionSink) {} + fn assert_backend_object(_: &mut dyn RenderBackend) {} + fn assert_composer_object(_: &mut dyn Game2DFrameComposer) {} + + struct CompileOnly; + + impl RenderSubmissionSink for CompileOnly { + fn submit_render_submission( + &mut self, + _submission: RenderSubmission, + ) -> Result<(), RenderSubmitError> { + Ok(()) + } + } + + impl RenderBackend for CompileOnly { + fn render_frame(&mut self) {} + } + + impl Game2DFrameComposer for CompileOnly { + fn begin_frame(&mut self) {} + + fn bind_scene(&mut self, _scene_bank_id: usize) -> ComposerOpStatus { + ComposerOpStatus::Ok + } + + fn unbind_scene(&mut self) {} + + fn set_camera(&mut self, _x: i32, _y: i32) -> ComposerOpStatus { + ComposerOpStatus::Ok + } + + fn emit_sprite(&mut self, _sprite: Sprite) -> ComposerOpStatus { + ComposerOpStatus::Ok + } + + fn close_game2d_packet(&self) -> Game2DFramePacket { + Game2DFramePacket::default() + } + + fn has_glyph_bank(&self, _bank_id: usize) -> bool { + false + } + } + + #[test] + fn core_platform_facades_are_object_safe() { + let mut compile_only = CompileOnly; + + assert_render_sink_object(&mut compile_only); + assert_backend_object(&mut compile_only); + assert_composer_object(&mut compile_only); + } + + #[test] + fn render_submit_error_is_typed_and_debuggable() { + assert_eq!(format!("{:?}", RenderSubmitError::BackendUnavailable), "BackendUnavailable"); + } +} From a268bd0b75164490a43f6779ac46e2601339fe2a Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 05:44:15 +0100 Subject: [PATCH 18/47] implements PLN-0099 --- .../console/prometeu-drivers/src/hardware.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index c04868cc..4159b9fa 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -12,6 +12,7 @@ use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::sprite::Sprite; use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge}; use prometeu_hal::{Game2DFramePacket, RenderSubmission, RenderSubmissionPacket}; +use prometeu_hal::{RenderSubmissionSink, RenderSubmitError}; use std::sync::Arc; /// Aggregate structure for all virtual hardware peripherals. @@ -138,6 +139,16 @@ impl HardwareBridge for Hardware { } } +impl RenderSubmissionSink for Hardware { + fn submit_render_submission( + &mut self, + submission: RenderSubmission, + ) -> Result<(), RenderSubmitError> { + self.publish_render_submission(&submission); + Ok(()) + } +} + impl Hardware { /// Internal hardware width in pixels. pub const W: usize = 480; @@ -197,6 +208,7 @@ mod tests { use prometeu_hal::tilemap::TileMap; use prometeu_hal::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, GameSpritePacket, Gfx2dCommand, + GfxUiCommand, ShellUiFramePacket, }; fn make_glyph_bank() -> GlyphBank { @@ -332,4 +344,31 @@ mod tests { assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw()); } + + #[test] + fn render_submission_sink_accepts_owned_game2d_submission() { + let mut hardware = Hardware::new(); + let packet = Game2DFramePacket::new( + ComposerFramePacket::default(), + vec![Gfx2dCommand::Clear { color: Color::BLUE }], + ); + let submission = RenderSubmission::game2d(FrameId::ZERO, packet); + + RenderSubmissionSink::submit_render_submission(&mut hardware, submission) + .expect("local sink should accept owned Game2D submissions"); + + assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); + } + + #[test] + fn render_submission_sink_accepts_owned_shell_ui_submission() { + let mut hardware = Hardware::new(); + let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]); + let submission = RenderSubmission::shell_ui(FrameId::ZERO, packet); + + RenderSubmissionSink::submit_render_submission(&mut hardware, submission) + .expect("local sink should accept owned ShellUi submissions"); + + assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw()); + } } From 18cfeedbff75e27cd5885b70cc0f1bf7e0669510 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 05:46:28 +0100 Subject: [PATCH 19/47] implements PLN-0100 --- .../console/prometeu-drivers/src/hardware.rs | 12 +++++++++-- .../firmware/firmware_step_crash_screen.rs | 9 ++++++-- .../firmware/firmware_step_splash_screen.rs | 9 ++++++-- crates/console/prometeu-hal/src/lib.rs | 4 ++-- crates/console/prometeu-hal/src/platform.rs | 21 +++++++++++++++++++ .../src/programs/prometeu_hub/prometeu_hub.rs | 7 +++++-- .../src/services/vm_runtime/tests.rs | 3 ++- .../src/services/vm_runtime/tick.rs | 13 +++++++----- 8 files changed, 62 insertions(+), 16 deletions(-) diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 4159b9fa..3eba94ea 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -302,7 +302,11 @@ mod tests { color: Color::BLUE, }]; - hardware.publish_render_submission(&RenderSubmission::game2d(FrameId::ZERO, packet)); + RenderSubmissionSink::submit_render_submission( + &mut hardware, + RenderSubmission::game2d(FrameId::ZERO, packet), + ) + .expect("local sink should publish Game2D packet"); assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); } @@ -340,7 +344,11 @@ mod tests { ); hardware.begin_frame(); - hardware.publish_render_submission(&RenderSubmission::game2d(FrameId::ZERO, packet)); + RenderSubmissionSink::submit_render_submission( + &mut hardware, + RenderSubmission::game2d(FrameId::ZERO, packet), + ) + .expect("local sink should publish Game2D packet"); assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw()); } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs index 8c5d6a52..7bbf14ac 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs @@ -2,7 +2,10 @@ use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep}; use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; -use prometeu_hal::{FrameId, GfxUiCommand, RenderSubmission, ShellUiFramePacket}; +use prometeu_hal::{ + FrameId, GfxUiCommand, LegacyHardwareRenderSubmissionSink, RenderSubmission, + RenderSubmissionSink, ShellUiFramePacket, +}; use prometeu_system::CrashReport; #[derive(Debug, Clone)] @@ -24,7 +27,9 @@ impl AppCrashesStep { ctx.hw.pad_mut().begin_frame(ctx.signals); let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]); - ctx.hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet)); + LegacyHardwareRenderSubmissionSink::new(ctx.hw) + .submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet)) + .expect("firmware crash render submission should publish"); // If START is pressed, return to the Hub if ctx.hw.pad().start().down { diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs index 5ba02ae9..0e49e7d5 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs @@ -3,7 +3,10 @@ use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::primitives::Rect; -use prometeu_hal::{FrameId, GfxUiCommand, RenderSubmission, ShellUiFramePacket}; +use prometeu_hal::{ + FrameId, GfxUiCommand, LegacyHardwareRenderSubmissionSink, RenderSubmission, + RenderSubmissionSink, ShellUiFramePacket, +}; #[derive(Debug, Clone)] pub struct SplashScreenStep { @@ -44,7 +47,9 @@ impl SplashScreenStep { color: Color::WHITE, }, ]); - ctx.hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet)); + LegacyHardwareRenderSubmissionSink::new(ctx.hw) + .submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet)) + .expect("firmware splash render submission should publish"); // Transition logic // If any button is pressed at any time after the animation ends diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index 70067fa1..59c60edd 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -49,8 +49,8 @@ pub use native_interface::{NativeInterface, SyscallId}; pub use pad_bridge::PadBridge; pub use platform::{ AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform, - RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform, TelemetryPlatform, - TestPlatform, + LegacyHardwareRenderSubmissionSink, RenderBackend, RenderSubmissionSink, RenderSubmitError, + RuntimePlatform, TelemetryPlatform, TestPlatform, }; pub use render_submission::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket, diff --git a/crates/console/prometeu-hal/src/platform.rs b/crates/console/prometeu-hal/src/platform.rs index 0cd8d3fc..7d448fda 100644 --- a/crates/console/prometeu-hal/src/platform.rs +++ b/crates/console/prometeu-hal/src/platform.rs @@ -1,6 +1,7 @@ use crate::asset_bridge::AssetBridge; use crate::audio_bridge::AudioBridge; use crate::composer_status::ComposerOpStatus; +use crate::hardware_bridge::HardwareBridge; use crate::pad_bridge::PadBridge; use crate::render_submission::{Game2DFramePacket, RenderSubmission}; use crate::sprite::Sprite; @@ -20,6 +21,26 @@ pub trait RenderSubmissionSink { ) -> Result<(), RenderSubmitError>; } +pub struct LegacyHardwareRenderSubmissionSink<'a> { + hw: &'a mut dyn HardwareBridge, +} + +impl<'a> LegacyHardwareRenderSubmissionSink<'a> { + pub fn new(hw: &'a mut dyn HardwareBridge) -> Self { + Self { hw } + } +} + +impl RenderSubmissionSink for LegacyHardwareRenderSubmissionSink<'_> { + fn submit_render_submission( + &mut self, + submission: RenderSubmission, + ) -> Result<(), RenderSubmitError> { + self.hw.publish_render_submission(&submission); + Ok(()) + } +} + pub trait RenderBackend { fn render_frame(&mut self); } diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index f2fb3624..d10f2db8 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -2,7 +2,8 @@ use crate::{CrashReport, SystemOS}; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; use prometeu_hal::{ - FrameId, GfxUiCommand, HardwareBridge, InputSignals, RenderSubmission, ShellUiFramePacket, + FrameId, GfxUiCommand, HardwareBridge, InputSignals, LegacyHardwareRenderSubmissionSink, + RenderSubmission, RenderSubmissionSink, ShellUiFramePacket, }; use prometeu_vm::VirtualMachine; @@ -142,7 +143,9 @@ impl PrometeuHub { } ShellUiFramePacket::new(commands) }; - hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet)); + LegacyHardwareRenderSubmissionSink::new(hw) + .submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet)) + .expect("hub render submission should publish"); } pub fn update_shell_profile( diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs index 66ed5bb4..49c75e91 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs @@ -858,7 +858,8 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() { first_packet.gfx2d[0], prometeu_hal::Gfx2dCommand::Clear { color } if color == Color::from_raw(0x11223344) )); - prometeu_hal::HardwareBridge::publish_render_submission(&mut hardware, latest); + prometeu_hal::RenderSubmissionSink::submit_render_submission(&mut hardware, latest.clone()) + .expect("local sink should publish latest submission"); assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x55667788).raw()); } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index a09672e8..1dd25816 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -9,8 +9,8 @@ use crate::services::memcard::MemcardService; use prometeu_hal::asset::{BankTelemetry, BankType}; use prometeu_hal::log::{LogLevel, LogService, LogSource}; use prometeu_hal::{ - FrameId, HardwareBridge, HostContext, InputSignals, RenderSubmission, RenderSubmissionPacket, - ShellUiFramePacket, + FrameId, HardwareBridge, HostContext, InputSignals, LegacyHardwareRenderSubmissionSink, + RenderSubmission, RenderSubmissionPacket, RenderSubmissionSink, ShellUiFramePacket, }; use prometeu_vm::LogicalFrameEndingReason; use std::collections::HashMap; @@ -18,12 +18,14 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::Ordering; struct HardwareRenderSurface<'a> { - hw: &'a mut dyn HardwareBridge, + sink: &'a mut dyn RenderSubmissionSink, } impl RenderSurface for HardwareRenderSurface<'_> { fn consume_submission(&mut self, submission: &RenderSubmission) { - self.hw.publish_render_submission(submission); + self.sink + .submit_render_submission(submission.clone()) + .expect("local render submission sink should publish"); } } @@ -262,7 +264,8 @@ impl VirtualMachineRuntime { } let render_outcome = catch_unwind(AssertUnwindSafe(|| { - let mut surface = HardwareRenderSurface { hw }; + let mut sink = LegacyHardwareRenderSubmissionSink::new(hw); + let mut surface = HardwareRenderSurface { sink: &mut sink }; match self .render_manager .active_render_policy() From 6f3df74e79b7d4838ced1248a654e7e11def9b4e Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 05:48:24 +0100 Subject: [PATCH 20/47] implements PLN-0101 --- .../src/services/vm_runtime/dispatch.rs | 14 --- .../src/services/vm_runtime/tests.rs | 99 +++++++++++++++++++ 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs index 723ad03c..1425f4fd 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs @@ -187,7 +187,6 @@ impl NativeInterface for VmRuntimeHost<'_> { Syscall::GfxClear => { let color = self.get_color(expect_int(args, 0)?)?; self.gfx2d_commands.push(Gfx2dCommand::Clear { color }); - hw.gfx_mut().clear(color); Ok(()) } Syscall::GfxUiClear => { @@ -199,7 +198,6 @@ impl NativeInterface for VmRuntimeHost<'_> { } let color = self.get_color(expect_int(args, 0)?)?; self.gfxui_commands.push(GfxUiCommand::Clear { color }); - hw.gfx_mut().clear(color); Ok(()) } Syscall::GfxFillRect => { @@ -210,7 +208,6 @@ impl NativeInterface for VmRuntimeHost<'_> { let color = self.get_color(expect_int(args, 4)?)?; self.gfx2d_commands .push(Gfx2dCommand::FillRect { rect: Rect { x, y, w, h }, color }); - hw.gfx_mut().fill_rect(x, y, w, h, color); Ok(()) } Syscall::GfxUiFillRect => { @@ -227,7 +224,6 @@ impl NativeInterface for VmRuntimeHost<'_> { let color = self.get_color(expect_int(args, 4)?)?; self.gfxui_commands .push(GfxUiCommand::FillRect { rect: Rect { x, y, w, h }, color }); - hw.gfx_mut().fill_rect(x, y, w, h, color); Ok(()) } Syscall::GfxDrawLine => { @@ -243,7 +239,6 @@ impl NativeInterface for VmRuntimeHost<'_> { y1: y2, color, }); - hw.gfx_mut().draw_line(x1, y1, x2, y2, color); Ok(()) } Syscall::GfxUiDrawLine => { @@ -265,7 +260,6 @@ impl NativeInterface for VmRuntimeHost<'_> { y1: y2, color, }); - hw.gfx_mut().draw_line(x1, y1, x2, y2, color); Ok(()) } Syscall::GfxDrawCircle => { @@ -274,7 +268,6 @@ impl NativeInterface for VmRuntimeHost<'_> { let r = expect_int(args, 2)? as i32; let color = self.get_color(expect_int(args, 3)?)?; self.gfx2d_commands.push(Gfx2dCommand::DrawCircle { x, y, radius: r, color }); - hw.gfx_mut().draw_circle(x, y, r, color); Ok(()) } Syscall::GfxUiDrawCircle => { @@ -289,7 +282,6 @@ impl NativeInterface for VmRuntimeHost<'_> { let r = expect_int(args, 2)? as i32; let color = self.get_color(expect_int(args, 3)?)?; self.gfxui_commands.push(GfxUiCommand::DrawCircle { x, y, radius: r, color }); - hw.gfx_mut().draw_circle(x, y, r, color); Ok(()) } Syscall::GfxDrawDisc => { @@ -305,7 +297,6 @@ impl NativeInterface for VmRuntimeHost<'_> { border_color, fill_color, }); - hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color); Ok(()) } Syscall::GfxUiDrawDisc => { @@ -327,7 +318,6 @@ impl NativeInterface for VmRuntimeHost<'_> { border_color, fill_color, }); - hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color); Ok(()) } Syscall::GfxDrawSquare => { @@ -342,7 +332,6 @@ impl NativeInterface for VmRuntimeHost<'_> { border_color, fill_color, }); - hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color); Ok(()) } Syscall::GfxUiDrawSquare => { @@ -363,7 +352,6 @@ impl NativeInterface for VmRuntimeHost<'_> { border_color, fill_color, }); - hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color); Ok(()) } Syscall::GfxDrawText => { @@ -372,7 +360,6 @@ impl NativeInterface for VmRuntimeHost<'_> { let msg = expect_string(args, 2, "message")?; let color = self.get_color(expect_int(args, 3)?)?; self.gfx2d_commands.push(Gfx2dCommand::DrawText { x, y, text: msg.clone(), color }); - hw.gfx_mut().draw_text(x, y, &msg, color); Ok(()) } Syscall::GfxUiDrawText => { @@ -387,7 +374,6 @@ impl NativeInterface for VmRuntimeHost<'_> { let msg = expect_string(args, 2, "message")?; let color = self.get_color(expect_int(args, 3)?)?; self.gfxui_commands.push(GfxUiCommand::DrawText { x, y, text: msg.clone(), color }); - hw.gfx_mut().draw_text(x, y, &msg, color); Ok(()) } Syscall::ComposerBindScene => { diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs index 49c75e91..4fa6e7b7 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs @@ -488,6 +488,105 @@ fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() { assert_eq!(runtime.frame_scheduler.active_game_frame_id(), None); } +#[test] +fn tick_gfx2d_clear_buffers_without_immediate_pixel_mutation_before_frame_close() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_I32 287454020\nHOSTCALL 0\nJMP 0").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx2d".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); + + assert!(report.is_none()); + assert!(runtime.render_manager.latest_complete_submission().is_none()); + assert!(matches!( + runtime.gfx2d_commands.first(), + Some(prometeu_hal::Gfx2dCommand::Clear { color }) + if *color == Color::from_raw(0x11223344) + )); + assert_ne!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); +} + +#[test] +fn tick_gfx2d_draw_text_buffers_without_immediate_pixel_mutation_before_frame_close() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = + assemble("PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 287454020\nHOSTCALL 0\nJMP 0") + .expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("I".into())], + vec![SyscallDecl { + module: "gfx2d".into(), + name: "draw_text".into(), + version: 1, + arg_slots: 4, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); + + assert!(report.is_none()); + assert!(runtime.render_manager.latest_complete_submission().is_none()); + assert!(matches!( + runtime.gfx2d_commands.first(), + Some(prometeu_hal::Gfx2dCommand::DrawText { x: 0, y: 0, text, color }) + if text == "I" && *color == Color::from_raw(0x11223344) + )); + assert_ne!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); +} + #[test] fn tick_system_profile_rejects_bank_game_surface() { let mut runtime = VirtualMachineRuntime::new(None); From d2bace261230b6204c64160b7779a1ed8dd34e22 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 05:51:07 +0100 Subject: [PATCH 21/47] implements PLN-0102 --- .../prometeu-drivers/src/frame_composer.rs | 78 ++++++++++++++++++- .../console/prometeu-drivers/src/hardware.rs | 51 ++++++++++-- crates/console/prometeu-hal/src/lib.rs | 4 +- crates/console/prometeu-hal/src/platform.rs | 54 +++++++++++-- .../src/services/vm_runtime/dispatch.rs | 27 +++---- 5 files changed, 184 insertions(+), 30 deletions(-) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index dd95bfda..80e3bd5a 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -8,8 +8,8 @@ use prometeu_hal::scene_viewport_resolver::{ }; use prometeu_hal::sprite::Sprite; use prometeu_hal::{ - BoundScenePacket, Camera2D, ComposerFramePacket, Game2DFramePacket, GameSpritePacket, - GfxBridge, HudPacket, + BoundScenePacket, Camera2D, ComposerFramePacket, ComposerOpStatus, Game2DFrameComposer, + Game2DFramePacket, GameSpritePacket, GfxBridge, HudPacket, }; use std::sync::Arc; @@ -452,6 +452,41 @@ impl FrameComposer { } } +impl Game2DFrameComposer for FrameComposer { + fn begin_frame(&mut self) { + FrameComposer::begin_frame(self); + } + + fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus { + if FrameComposer::bind_scene(self, scene_bank_id) { + ComposerOpStatus::Ok + } else { + ComposerOpStatus::SceneUnavailable + } + } + + fn unbind_scene(&mut self) { + FrameComposer::unbind_scene(self); + } + + fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus { + FrameComposer::set_camera(self, x, y); + ComposerOpStatus::Ok + } + + fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus { + if FrameComposer::emit_sprite(self, sprite) { + ComposerOpStatus::Ok + } else { + ComposerOpStatus::SpriteOverflow + } + } + + fn close_game2d_packet(&self) -> Game2DFramePacket { + FrameComposer::close_game2d_packet(self) + } +} + #[cfg(test)] mod tests { use super::*; @@ -896,6 +931,45 @@ mod tests { assert!(packet.gfx2d.is_empty()); } + #[test] + fn game2d_frame_composer_facade_closes_equivalent_packet() { + let mut frame_composer = FrameComposer::new( + Hardware::W, + Hardware::H, + Arc::new(MemoryBanks::default()), + make_glyph_slot_index(&[]), + ); + + let composer: &mut dyn Game2DFrameComposer = &mut frame_composer; + assert_eq!(composer.set_camera(10, 20), ComposerOpStatus::Ok); + composer.begin_frame(); + assert_eq!( + composer.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 7, palette_id: 3 }, + x: 4, + y: 5, + layer: 2, + bank_id: 1, + active: false, + flip_x: true, + flip_y: false, + priority: 9, + }), + ComposerOpStatus::Ok + ); + + let packet = composer.close_game2d_packet(); + + assert_eq!(packet.composer.bound_scene, None); + assert_eq!(packet.composer.camera, Camera2D { x: 10, y: 20 }); + assert_eq!(packet.composer.sprites.len(), 1); + assert_eq!(packet.composer.sprites[0].glyph_id, 7); + assert_eq!(packet.composer.sprites[0].palette_id, 3); + assert_eq!(packet.composer.sprites[0].layer, 2); + assert!(packet.composer.sprites[0].flip_x); + assert!(packet.gfx2d.is_empty()); + } + #[test] fn composer_buffer_preserves_integer_layer_ordering() { let mut frame_composer = FrameComposer::new( diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 3eba94ea..63e9b2be 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -11,7 +11,9 @@ use crate::touch::Touch; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::sprite::Sprite; use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge}; -use prometeu_hal::{Game2DFramePacket, RenderSubmission, RenderSubmissionPacket}; +use prometeu_hal::{ + Game2DFrameComposer, Game2DFramePacket, RenderSubmission, RenderSubmissionPacket, +}; use prometeu_hal::{RenderSubmissionSink, RenderSubmitError}; use std::sync::Arc; @@ -149,6 +151,45 @@ impl RenderSubmissionSink for Hardware { } } +impl Game2DFrameComposer for Hardware { + fn begin_frame(&mut self) { + self.frame_composer.begin_frame(); + } + + fn bind_scene(&mut self, scene_bank_id: usize) -> prometeu_hal::ComposerOpStatus { + if self.frame_composer.bind_scene(scene_bank_id) { + prometeu_hal::ComposerOpStatus::Ok + } else { + prometeu_hal::ComposerOpStatus::SceneUnavailable + } + } + + fn unbind_scene(&mut self) { + self.frame_composer.unbind_scene(); + } + + fn set_camera(&mut self, x: i32, y: i32) -> prometeu_hal::ComposerOpStatus { + self.frame_composer.set_camera(x, y); + prometeu_hal::ComposerOpStatus::Ok + } + + fn emit_sprite(&mut self, sprite: Sprite) -> prometeu_hal::ComposerOpStatus { + if !self.has_glyph_bank(sprite.bank_id as usize) { + return prometeu_hal::ComposerOpStatus::BankInvalid; + } + + if self.frame_composer.emit_sprite(sprite) { + prometeu_hal::ComposerOpStatus::Ok + } else { + prometeu_hal::ComposerOpStatus::SpriteOverflow + } + } + + fn close_game2d_packet(&self) -> Game2DFramePacket { + self.frame_composer.close_game2d_packet() + } +} + impl Hardware { /// Internal hardware width in pixels. pub const W: usize = 480; @@ -294,9 +335,9 @@ mod tests { let mut glyph_slots = [None; 16]; glyph_slots[0] = Some(0); hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots); - assert!(hardware.bind_scene(0)); + assert!(HardwareBridge::bind_scene(&mut hardware, 0)); - let mut packet = hardware.close_game2d_packet(); + let mut packet = HardwareBridge::close_game2d_packet(&hardware); packet.gfx2d = vec![Gfx2dCommand::FillRect { rect: Rect { x: 0, y: 0, w: 1, h: 1 }, color: Color::BLUE, @@ -321,7 +362,7 @@ mod tests { let mut glyph_slots = [None; 16]; glyph_slots[0] = Some(0); hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots); - assert!(hardware.bind_scene(0)); + assert!(HardwareBridge::bind_scene(&mut hardware, 0)); let packet = Game2DFramePacket::new( ComposerFramePacket { @@ -343,7 +384,7 @@ mod tests { Vec::new(), ); - hardware.begin_frame(); + HardwareBridge::begin_frame(&mut hardware); RenderSubmissionSink::submit_render_submission( &mut hardware, RenderSubmission::game2d(FrameId::ZERO, packet), diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index 59c60edd..517b8099 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -49,8 +49,8 @@ pub use native_interface::{NativeInterface, SyscallId}; pub use pad_bridge::PadBridge; pub use platform::{ AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform, - LegacyHardwareRenderSubmissionSink, RenderBackend, RenderSubmissionSink, RenderSubmitError, - RuntimePlatform, TelemetryPlatform, TestPlatform, + LegacyHardwareGame2DFrameComposer, LegacyHardwareRenderSubmissionSink, RenderBackend, + RenderSubmissionSink, RenderSubmitError, RuntimePlatform, TelemetryPlatform, TestPlatform, }; pub use render_submission::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket, diff --git a/crates/console/prometeu-hal/src/platform.rs b/crates/console/prometeu-hal/src/platform.rs index 7d448fda..57b1d1a9 100644 --- a/crates/console/prometeu-hal/src/platform.rs +++ b/crates/console/prometeu-hal/src/platform.rs @@ -52,7 +52,55 @@ pub trait Game2DFrameComposer { fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus; fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus; fn close_game2d_packet(&self) -> Game2DFramePacket; - fn has_glyph_bank(&self, bank_id: usize) -> bool; +} + +pub struct LegacyHardwareGame2DFrameComposer<'a> { + hw: &'a mut dyn HardwareBridge, +} + +impl<'a> LegacyHardwareGame2DFrameComposer<'a> { + pub fn new(hw: &'a mut dyn HardwareBridge) -> Self { + Self { hw } + } +} + +impl Game2DFrameComposer for LegacyHardwareGame2DFrameComposer<'_> { + fn begin_frame(&mut self) { + self.hw.begin_frame(); + } + + fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus { + if self.hw.bind_scene(scene_bank_id) { + ComposerOpStatus::Ok + } else { + ComposerOpStatus::SceneUnavailable + } + } + + fn unbind_scene(&mut self) { + self.hw.unbind_scene(); + } + + fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus { + self.hw.set_camera(x, y); + ComposerOpStatus::Ok + } + + fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus { + if !self.hw.has_glyph_bank(sprite.bank_id as usize) { + return ComposerOpStatus::BankInvalid; + } + + if self.hw.emit_sprite(sprite) { + ComposerOpStatus::Ok + } else { + ComposerOpStatus::SpriteOverflow + } + } + + fn close_game2d_packet(&self) -> Game2DFramePacket { + self.hw.close_game2d_packet() + } } pub trait AudioPlatform: AudioBridge {} @@ -135,10 +183,6 @@ mod tests { fn close_game2d_packet(&self) -> Game2DFramePacket { Game2DFramePacket::default() } - - fn has_glyph_bank(&self, _bank_id: usize) -> bool { - false - } } #[test] diff --git a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs index 1425f4fd..0106a653 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs @@ -12,8 +12,9 @@ use prometeu_hal::sprite::Sprite; use prometeu_hal::syscalls::Syscall; use prometeu_hal::vm_fault::VmFault; use prometeu_hal::{ - AudioOpStatus, ComposerOpStatus, Gfx2dCommand, GfxUiCommand, HostContext, HostReturn, - NativeInterface, SyscallId, expect_bool, expect_int, + AudioOpStatus, ComposerOpStatus, Game2DFrameComposer, Gfx2dCommand, GfxUiCommand, HostContext, + HostReturn, LegacyHardwareGame2DFrameComposer, NativeInterface, SyscallId, expect_bool, + expect_int, }; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; @@ -385,23 +386,24 @@ impl NativeInterface for VmRuntimeHost<'_> { } }; - let bound = catch_unwind(AssertUnwindSafe(|| hw.bind_scene(scene_bank_id))) - .map_err(|payload| VmFault::Panic(Self::panic_payload_to_string(payload)))?; + let status = catch_unwind(AssertUnwindSafe(|| { + let mut composer = LegacyHardwareGame2DFrameComposer::new(hw); + composer.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 }; ret.push_int(status as i64); Ok(()) } Syscall::ComposerUnbindScene => { - hw.unbind_scene(); + LegacyHardwareGame2DFrameComposer::new(hw).unbind_scene(); ret.push_int(ComposerOpStatus::Ok as i64); Ok(()) } Syscall::ComposerSetCamera => { let x = Self::int_arg_to_i32_trap(expect_int(args, 0)?, "camera x")?; let y = Self::int_arg_to_i32_trap(expect_int(args, 1)?, "camera y")?; - hw.set_camera(x, y); + LegacyHardwareGame2DFrameComposer::new(hw).set_camera(x, y); Ok(()) } Syscall::ComposerEmitSprite => { @@ -449,12 +451,7 @@ impl NativeInterface for VmRuntimeHost<'_> { } }; - if !hw.has_glyph_bank(bank_id as usize) { - ret.push_int(ComposerOpStatus::BankInvalid as i64); - return Ok(()); - } - - let emitted = hw.emit_sprite(Sprite { + let status = LegacyHardwareGame2DFrameComposer::new(hw).emit_sprite(Sprite { glyph: Glyph { glyph_id, palette_id }, x, y, @@ -465,8 +462,6 @@ impl NativeInterface for VmRuntimeHost<'_> { flip_y, priority, }); - let status = - if emitted { ComposerOpStatus::Ok } else { ComposerOpStatus::SpriteOverflow }; ret.push_int(status as i64); Ok(()) } From c62aef51ba10b491fb0ad1a5a7c4aba3bea3f76e Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 05:52:48 +0100 Subject: [PATCH 22/47] implements PLN-0103 --- .../console/prometeu-drivers/src/hardware.rs | 73 ++++++++++++- crates/console/prometeu-drivers/src/lib.rs | 2 + .../prometeu-drivers/src/test_platform.rs | 100 ++++++++++++++++++ crates/console/prometeu-hal/src/lib.rs | 5 +- crates/console/prometeu-hal/src/platform.rs | 5 + 5 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 crates/console/prometeu-drivers/src/test_platform.rs diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 63e9b2be..30349a28 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -10,7 +10,10 @@ use crate::pad::Pad; use crate::touch::Touch; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::sprite::Sprite; -use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge}; +use prometeu_hal::{ + AssetBridge, AudioBridge, GfxBridge, HardwareBridge, InputPlatform, NoopTelemetryPlatform, + PadBridge, RenderBackend, RuntimePlatform, TelemetryPlatform, TouchBridge, +}; use prometeu_hal::{ Game2DFrameComposer, Game2DFramePacket, RenderSubmission, RenderSubmissionPacket, }; @@ -151,6 +154,12 @@ impl RenderSubmissionSink for Hardware { } } +impl RenderBackend for Hardware { + fn render_frame(&mut self) { + HardwareBridge::render_frame(self); + } +} + impl Game2DFrameComposer for Hardware { fn begin_frame(&mut self) { self.frame_composer.begin_frame(); @@ -190,6 +199,68 @@ impl Game2DFrameComposer for Hardware { } } +impl InputPlatform for Hardware { + fn pad(&self) -> &dyn PadBridge { + &self.pad + } + + fn pad_mut(&mut self) -> &mut dyn PadBridge { + &mut self.pad + } + + fn touch(&self) -> &dyn TouchBridge { + &self.touch + } + + fn touch_mut(&mut self) -> &mut dyn TouchBridge { + &mut self.touch + } +} + +static NOOP_TELEMETRY: NoopTelemetryPlatform = NoopTelemetryPlatform; + +impl RuntimePlatform for Hardware { + fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink { + self + } + + fn render_backend(&mut self) -> &mut dyn RenderBackend { + self + } + + fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer { + self + } + + fn audio(&self) -> &dyn prometeu_hal::AudioPlatform { + &self.audio + } + + fn audio_mut(&mut self) -> &mut dyn prometeu_hal::AudioPlatform { + &mut self.audio + } + + fn input(&self) -> &dyn InputPlatform { + self + } + + fn input_mut(&mut self) -> &mut dyn InputPlatform { + self + } + + fn assets(&self) -> &dyn prometeu_hal::AssetStoragePlatform { + &self.assets + } + + fn assets_mut(&mut self) -> &mut dyn prometeu_hal::AssetStoragePlatform { + &mut self.assets + } + + fn telemetry(&self) -> &dyn TelemetryPlatform { + &NOOP_TELEMETRY + } +} + impl Hardware { /// Internal hardware width in pixels. pub const W: usize = 480; diff --git a/crates/console/prometeu-drivers/src/lib.rs b/crates/console/prometeu-drivers/src/lib.rs index d5173830..dacd2eb2 100644 --- a/crates/console/prometeu-drivers/src/lib.rs +++ b/crates/console/prometeu-drivers/src/lib.rs @@ -5,6 +5,7 @@ mod gfx; pub mod hardware; mod memory_banks; mod pad; +mod test_platform; mod touch; pub use crate::asset::AssetManager; @@ -16,3 +17,4 @@ pub use crate::memory_banks::{ SceneBankPoolAccess, SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller, }; pub use crate::pad::Pad; +pub use crate::test_platform::TestPlatform; diff --git a/crates/console/prometeu-drivers/src/test_platform.rs b/crates/console/prometeu-drivers/src/test_platform.rs new file mode 100644 index 00000000..3769d964 --- /dev/null +++ b/crates/console/prometeu-drivers/src/test_platform.rs @@ -0,0 +1,100 @@ +use crate::hardware::Hardware; +use crate::memory_banks::MemoryBanks; +use prometeu_hal::{ + AssetStoragePlatform, AudioPlatform, Game2DFrameComposer, InputPlatform, RenderBackend, + RenderSubmissionSink, RuntimePlatform, TelemetryPlatform, +}; +use std::sync::Arc; + +pub struct TestPlatform { + hardware: Hardware, +} + +impl Default for TestPlatform { + fn default() -> Self { + Self::new() + } +} + +impl TestPlatform { + pub fn new() -> Self { + Self { hardware: Hardware::new() } + } + + pub fn new_with_memory_banks(memory_banks: Arc) -> Self { + Self { hardware: Hardware::new_with_memory_banks(memory_banks) } + } + + pub fn local_hardware(&self) -> &Hardware { + &self.hardware + } + + pub fn local_hardware_mut(&mut self) -> &mut Hardware { + &mut self.hardware + } +} + +impl RuntimePlatform for TestPlatform { + fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink { + self.hardware.render_submission_sink() + } + + fn render_backend(&mut self) -> &mut dyn RenderBackend { + self.hardware.render_backend() + } + + fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer { + self.hardware.game2d_frame_composer() + } + + fn audio(&self) -> &dyn AudioPlatform { + self.hardware.audio() + } + + fn audio_mut(&mut self) -> &mut dyn AudioPlatform { + self.hardware.audio_mut() + } + + fn input(&self) -> &dyn InputPlatform { + self.hardware.input() + } + + fn input_mut(&mut self) -> &mut dyn InputPlatform { + self.hardware.input_mut() + } + + fn assets(&self) -> &dyn AssetStoragePlatform { + self.hardware.assets() + } + + fn assets_mut(&mut self) -> &mut dyn AssetStoragePlatform { + self.hardware.assets_mut() + } + + fn telemetry(&self) -> &dyn TelemetryPlatform { + self.hardware.telemetry() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use prometeu_hal::{FrameId, Game2DFramePacket, RenderSubmission}; + + #[test] + fn test_platform_initializes_required_facades() { + let mut platform = TestPlatform::new(); + + platform.game2d_frame_composer().begin_frame(); + platform + .render_submission_sink() + .submit_render_submission(RenderSubmission::game2d( + FrameId::ZERO, + Game2DFramePacket::default(), + )) + .expect("test platform render sink should accept local submission"); + assert!(!platform.audio().is_playing(0)); + assert!(!platform.input().pad().any()); + assert!(platform.telemetry().telemetry_snapshot().is_none()); + } +} diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index 517b8099..868b1401 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -49,8 +49,9 @@ pub use native_interface::{NativeInterface, SyscallId}; pub use pad_bridge::PadBridge; pub use platform::{ AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform, - LegacyHardwareGame2DFrameComposer, LegacyHardwareRenderSubmissionSink, RenderBackend, - RenderSubmissionSink, RenderSubmitError, RuntimePlatform, TelemetryPlatform, TestPlatform, + LegacyHardwareGame2DFrameComposer, LegacyHardwareRenderSubmissionSink, NoopTelemetryPlatform, + RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform, TelemetryPlatform, + TestPlatform, }; pub use render_submission::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket, diff --git a/crates/console/prometeu-hal/src/platform.rs b/crates/console/prometeu-hal/src/platform.rs index 57b1d1a9..e50dc0ed 100644 --- a/crates/console/prometeu-hal/src/platform.rs +++ b/crates/console/prometeu-hal/src/platform.rs @@ -124,6 +124,11 @@ pub trait TelemetryPlatform { } } +#[derive(Debug, Default)] +pub struct NoopTelemetryPlatform; + +impl TelemetryPlatform for NoopTelemetryPlatform {} + pub trait RuntimePlatform { fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink; fn render_backend(&mut self) -> &mut dyn RenderBackend; From d0a2682366050be3f33f7549c5691247cffd9ed2 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 05:56:15 +0100 Subject: [PATCH 23/47] implements PLN-0104 --- .../console/prometeu-drivers/src/hardware.rs | 8 +- .../prometeu-drivers/src/test_platform.rs | 10 +- .../console/prometeu-hal/src/host_context.rs | 16 ++- crates/console/prometeu-hal/src/lib.rs | 6 +- crates/console/prometeu-hal/src/platform.rs | 135 +++++++++++++++++- .../src/services/vm_runtime/dispatch.rs | 33 +++-- .../src/services/vm_runtime/tick.rs | 34 +++-- 7 files changed, 193 insertions(+), 49 deletions(-) diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 30349a28..244518e1 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -232,11 +232,11 @@ impl RuntimePlatform for Hardware { self } - fn audio(&self) -> &dyn prometeu_hal::AudioPlatform { + fn audio(&self) -> &dyn AudioBridge { &self.audio } - fn audio_mut(&mut self) -> &mut dyn prometeu_hal::AudioPlatform { + fn audio_mut(&mut self) -> &mut dyn AudioBridge { &mut self.audio } @@ -248,11 +248,11 @@ impl RuntimePlatform for Hardware { self } - fn assets(&self) -> &dyn prometeu_hal::AssetStoragePlatform { + fn assets(&self) -> &dyn AssetBridge { &self.assets } - fn assets_mut(&mut self) -> &mut dyn prometeu_hal::AssetStoragePlatform { + fn assets_mut(&mut self) -> &mut dyn AssetBridge { &mut self.assets } diff --git a/crates/console/prometeu-drivers/src/test_platform.rs b/crates/console/prometeu-drivers/src/test_platform.rs index 3769d964..1b34910b 100644 --- a/crates/console/prometeu-drivers/src/test_platform.rs +++ b/crates/console/prometeu-drivers/src/test_platform.rs @@ -1,7 +1,7 @@ use crate::hardware::Hardware; use crate::memory_banks::MemoryBanks; use prometeu_hal::{ - AssetStoragePlatform, AudioPlatform, Game2DFrameComposer, InputPlatform, RenderBackend, + AssetBridge, AudioBridge, Game2DFrameComposer, InputPlatform, RenderBackend, RenderSubmissionSink, RuntimePlatform, TelemetryPlatform, }; use std::sync::Arc; @@ -47,11 +47,11 @@ impl RuntimePlatform for TestPlatform { self.hardware.game2d_frame_composer() } - fn audio(&self) -> &dyn AudioPlatform { + fn audio(&self) -> &dyn AudioBridge { self.hardware.audio() } - fn audio_mut(&mut self) -> &mut dyn AudioPlatform { + fn audio_mut(&mut self) -> &mut dyn AudioBridge { self.hardware.audio_mut() } @@ -63,11 +63,11 @@ impl RuntimePlatform for TestPlatform { self.hardware.input_mut() } - fn assets(&self) -> &dyn AssetStoragePlatform { + fn assets(&self) -> &dyn AssetBridge { self.hardware.assets() } - fn assets_mut(&mut self) -> &mut dyn AssetStoragePlatform { + fn assets_mut(&mut self) -> &mut dyn AssetBridge { self.hardware.assets_mut() } diff --git a/crates/console/prometeu-hal/src/host_context.rs b/crates/console/prometeu-hal/src/host_context.rs index 714d44df..b4ec4862 100644 --- a/crates/console/prometeu-hal/src/host_context.rs +++ b/crates/console/prometeu-hal/src/host_context.rs @@ -1,13 +1,19 @@ use crate::hardware_bridge::HardwareBridge; +use crate::platform::RuntimePlatform; use crate::vm_fault::VmFault; pub struct HostContext<'a> { pub hw: Option<&'a mut dyn HardwareBridge>, + pub platform: Option<&'a mut dyn RuntimePlatform>, } impl<'a> HostContext<'a> { pub fn new(hw: Option<&'a mut dyn HardwareBridge>) -> Self { - Self { hw } + Self { hw, platform: None } + } + + pub fn new_platform(platform: Option<&'a mut dyn RuntimePlatform>) -> Self { + Self { hw: None, platform } } #[inline] @@ -17,6 +23,14 @@ impl<'a> HostContext<'a> { None => Err(VmFault::Unavailable), } } + + #[inline] + pub fn require_platform(&mut self) -> Result<&mut dyn RuntimePlatform, VmFault> { + match &mut self.platform { + Some(platform) => Ok(*platform), + None => Err(VmFault::Unavailable), + } + } } pub trait HostContextProvider { diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index 868b1401..1805e122 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -49,9 +49,9 @@ pub use native_interface::{NativeInterface, SyscallId}; pub use pad_bridge::PadBridge; pub use platform::{ AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform, - LegacyHardwareGame2DFrameComposer, LegacyHardwareRenderSubmissionSink, NoopTelemetryPlatform, - RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform, TelemetryPlatform, - TestPlatform, + LegacyHardwareGame2DFrameComposer, LegacyHardwareRenderSubmissionSink, + LegacyHardwareRuntimePlatform, NoopTelemetryPlatform, RenderBackend, RenderSubmissionSink, + RenderSubmitError, RuntimePlatform, TelemetryPlatform, TestPlatform, }; pub use render_submission::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket, diff --git a/crates/console/prometeu-hal/src/platform.rs b/crates/console/prometeu-hal/src/platform.rs index e50dc0ed..68cc42b0 100644 --- a/crates/console/prometeu-hal/src/platform.rs +++ b/crates/console/prometeu-hal/src/platform.rs @@ -103,6 +103,89 @@ impl Game2DFrameComposer for LegacyHardwareGame2DFrameComposer<'_> { } } +pub struct LegacyHardwareRuntimePlatform<'a> { + hw: &'a mut dyn HardwareBridge, +} + +impl<'a> LegacyHardwareRuntimePlatform<'a> { + pub fn new(hw: &'a mut dyn HardwareBridge) -> Self { + Self { hw } + } +} + +impl RenderSubmissionSink for LegacyHardwareRuntimePlatform<'_> { + fn submit_render_submission( + &mut self, + submission: RenderSubmission, + ) -> Result<(), RenderSubmitError> { + self.hw.publish_render_submission(&submission); + Ok(()) + } +} + +impl RenderBackend for LegacyHardwareRuntimePlatform<'_> { + fn render_frame(&mut self) { + self.hw.render_frame(); + } +} + +impl Game2DFrameComposer for LegacyHardwareRuntimePlatform<'_> { + fn begin_frame(&mut self) { + self.hw.begin_frame(); + } + + fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus { + if self.hw.bind_scene(scene_bank_id) { + ComposerOpStatus::Ok + } else { + ComposerOpStatus::SceneUnavailable + } + } + + fn unbind_scene(&mut self) { + self.hw.unbind_scene(); + } + + fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus { + self.hw.set_camera(x, y); + ComposerOpStatus::Ok + } + + fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus { + if !self.hw.has_glyph_bank(sprite.bank_id as usize) { + return ComposerOpStatus::BankInvalid; + } + + if self.hw.emit_sprite(sprite) { + ComposerOpStatus::Ok + } else { + ComposerOpStatus::SpriteOverflow + } + } + + fn close_game2d_packet(&self) -> Game2DFramePacket { + self.hw.close_game2d_packet() + } +} + +impl InputPlatform for LegacyHardwareRuntimePlatform<'_> { + fn pad(&self) -> &dyn PadBridge { + self.hw.pad() + } + + fn pad_mut(&mut self) -> &mut dyn PadBridge { + self.hw.pad_mut() + } + + fn touch(&self) -> &dyn TouchBridge { + self.hw.touch() + } + + fn touch_mut(&mut self) -> &mut dyn TouchBridge { + self.hw.touch_mut() + } +} + pub trait AudioPlatform: AudioBridge {} impl AudioPlatform for T {} @@ -129,19 +212,63 @@ pub struct NoopTelemetryPlatform; impl TelemetryPlatform for NoopTelemetryPlatform {} +static LEGACY_NOOP_TELEMETRY: NoopTelemetryPlatform = NoopTelemetryPlatform; + pub trait RuntimePlatform { fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink; fn render_backend(&mut self) -> &mut dyn RenderBackend; fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer; - fn audio(&self) -> &dyn AudioPlatform; - fn audio_mut(&mut self) -> &mut dyn AudioPlatform; + fn audio(&self) -> &dyn AudioBridge; + fn audio_mut(&mut self) -> &mut dyn AudioBridge; fn input(&self) -> &dyn InputPlatform; fn input_mut(&mut self) -> &mut dyn InputPlatform; - fn assets(&self) -> &dyn AssetStoragePlatform; - fn assets_mut(&mut self) -> &mut dyn AssetStoragePlatform; + fn assets(&self) -> &dyn AssetBridge; + fn assets_mut(&mut self) -> &mut dyn AssetBridge; fn telemetry(&self) -> &dyn TelemetryPlatform; } +impl RuntimePlatform for LegacyHardwareRuntimePlatform<'_> { + fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink { + self + } + + fn render_backend(&mut self) -> &mut dyn RenderBackend { + self + } + + fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer { + self + } + + fn audio(&self) -> &dyn AudioBridge { + self.hw.audio() + } + + fn audio_mut(&mut self) -> &mut dyn AudioBridge { + self.hw.audio_mut() + } + + fn input(&self) -> &dyn InputPlatform { + self + } + + fn input_mut(&mut self) -> &mut dyn InputPlatform { + self + } + + fn assets(&self) -> &dyn AssetBridge { + self.hw.assets() + } + + fn assets_mut(&mut self) -> &mut dyn AssetBridge { + self.hw.assets_mut() + } + + fn telemetry(&self) -> &dyn TelemetryPlatform { + &LEGACY_NOOP_TELEMETRY + } +} + pub trait TestPlatform: RuntimePlatform {} impl TestPlatform for T {} diff --git a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs index 0106a653..932b8589 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs @@ -12,9 +12,8 @@ use prometeu_hal::sprite::Sprite; use prometeu_hal::syscalls::Syscall; use prometeu_hal::vm_fault::VmFault; use prometeu_hal::{ - AudioOpStatus, ComposerOpStatus, Game2DFrameComposer, Gfx2dCommand, GfxUiCommand, HostContext, - HostReturn, LegacyHardwareGame2DFrameComposer, NativeInterface, SyscallId, expect_bool, - expect_int, + AudioOpStatus, ComposerOpStatus, Gfx2dCommand, GfxUiCommand, HostContext, HostReturn, + NativeInterface, SyscallId, expect_bool, expect_int, }; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; @@ -180,7 +179,7 @@ impl NativeInterface for VmRuntimeHost<'_> { self.ensure_game_profile_syscall(syscall)?; - let hw = ctx.require_hw()?; + let platform = ctx.require_platform()?; match syscall { Syscall::SystemHasCart => unreachable!(), @@ -387,8 +386,7 @@ impl NativeInterface for VmRuntimeHost<'_> { }; let status = catch_unwind(AssertUnwindSafe(|| { - let mut composer = LegacyHardwareGame2DFrameComposer::new(hw); - composer.bind_scene(scene_bank_id) + platform.game2d_frame_composer().bind_scene(scene_bank_id) })) .map_err(|payload| VmFault::Panic(Self::panic_payload_to_string(payload)))?; @@ -396,14 +394,14 @@ impl NativeInterface for VmRuntimeHost<'_> { Ok(()) } Syscall::ComposerUnbindScene => { - LegacyHardwareGame2DFrameComposer::new(hw).unbind_scene(); + platform.game2d_frame_composer().unbind_scene(); ret.push_int(ComposerOpStatus::Ok as i64); Ok(()) } Syscall::ComposerSetCamera => { let x = Self::int_arg_to_i32_trap(expect_int(args, 0)?, "camera x")?; let y = Self::int_arg_to_i32_trap(expect_int(args, 1)?, "camera y")?; - LegacyHardwareGame2DFrameComposer::new(hw).set_camera(x, y); + platform.game2d_frame_composer().set_camera(x, y); Ok(()) } Syscall::ComposerEmitSprite => { @@ -451,7 +449,7 @@ impl NativeInterface for VmRuntimeHost<'_> { } }; - let status = LegacyHardwareGame2DFrameComposer::new(hw).emit_sprite(Sprite { + let status = platform.game2d_frame_composer().emit_sprite(Sprite { glyph: Glyph { glyph_id, palette_id }, x, y, @@ -488,7 +486,7 @@ impl NativeInterface for VmRuntimeHost<'_> { return Ok(()); } - let status = hw.audio_mut().play( + let status = platform.audio_mut().play( 0, sample_id_raw as u16, voice_id_raw as usize, @@ -518,7 +516,8 @@ impl NativeInterface for VmRuntimeHost<'_> { return Ok(()); } - if hw.assets().slot_info(SlotRef::audio(bank_id as usize)).asset_id.is_none() { + if platform.assets().slot_info(SlotRef::audio(bank_id as usize)).asset_id.is_none() + { ret.push_int(AudioOpStatus::BankInvalid as i64); return Ok(()); } @@ -534,7 +533,7 @@ impl NativeInterface for VmRuntimeHost<'_> { return Ok(()); } - let status = hw.audio_mut().play( + let status = platform.audio_mut().play( bank_id, sample_id_raw as u16, voice_id_raw as usize, @@ -731,7 +730,7 @@ impl NativeInterface for VmRuntimeHost<'_> { })?; let slot_index = expect_int(args, 1)? as usize; - match hw.assets().load(asset_id, slot_index) { + match platform.assets().load(asset_id, slot_index) { Ok(handle) => { ret.push_int(AssetOpStatus::Ok as i64); ret.push_int(handle as i64); @@ -745,16 +744,16 @@ impl NativeInterface for VmRuntimeHost<'_> { } } Syscall::AssetStatus => { - ret.push_int(hw.assets().status(expect_int(args, 0)? as u32) as i64); + ret.push_int(platform.assets().status(expect_int(args, 0)? as u32) as i64); Ok(()) } Syscall::AssetCommit => { - let status = hw.assets().commit(expect_int(args, 0)? as u32); + let status = platform.assets().commit(expect_int(args, 0)? as u32); ret.push_int(status as i64); Ok(()) } Syscall::AssetCancel => { - let status = hw.assets().cancel(expect_int(args, 0)? as u32); + let status = platform.assets().cancel(expect_int(args, 0)? as u32); ret.push_int(status as i64); Ok(()) } @@ -764,7 +763,7 @@ impl NativeInterface for VmRuntimeHost<'_> { 1 => BankType::SOUNDS, _ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())), }; - let telemetry = hw + let telemetry = platform .assets() .bank_telemetry() .into_iter() diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index 1dd25816..0aac4cb7 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -9,8 +9,9 @@ use crate::services::memcard::MemcardService; use prometeu_hal::asset::{BankTelemetry, BankType}; use prometeu_hal::log::{LogLevel, LogService, LogSource}; use prometeu_hal::{ - FrameId, HardwareBridge, HostContext, InputSignals, LegacyHardwareRenderSubmissionSink, - RenderSubmission, RenderSubmissionPacket, RenderSubmissionSink, ShellUiFramePacket, + FrameId, HardwareBridge, HostContext, InputSignals, LegacyHardwareRuntimePlatform, + RenderSubmission, RenderSubmissionPacket, RenderSubmissionSink, RuntimePlatform, + ShellUiFramePacket, }; use prometeu_vm::LogicalFrameEndingReason; use std::collections::HashMap; @@ -53,9 +54,9 @@ impl VirtualMachineRuntime { } fn bank_telemetry_summary( - hw: &dyn HardwareBridge, + platform: &dyn RuntimePlatform, ) -> (BankTelemetry, BankTelemetry, BankTelemetry) { - let telemetry = hw.assets().bank_telemetry(); + let telemetry = platform.assets().bank_telemetry(); let glyph = telemetry.iter().find(|entry| entry.bank_type == BankType::GLYPH).cloned().unwrap_or( BankTelemetry { bank_type: BankType::GLYPH, used_slots: 0, total_slots: 0 }, @@ -79,7 +80,8 @@ impl VirtualMachineRuntime { hw: &mut dyn HardwareBridge, ) -> Option { let step_result = { - let mut ctx = HostContext::new(Some(hw)); + let mut platform = LegacyHardwareRuntimePlatform::new(hw); + let mut ctx = HostContext::new_platform(Some(&mut platform)); let mut fs = VirtualFS::new(); let mut fs_state = FsState::Unmounted; let mut memcard = MemcardService::new(); @@ -143,6 +145,7 @@ impl VirtualMachineRuntime { } self.update_fs(log_service, fs, fs_state); + let mut platform = LegacyHardwareRuntimePlatform::new(hw); if !self.logical_frame_active { if RenderManager::policy_for_app_mode(self.current_cartridge_app_mode) @@ -154,7 +157,7 @@ impl VirtualMachineRuntime { } self.logical_frame_active = true; self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME; - self.begin_logical_frame(signals, hw); + self.begin_logical_frame(signals, &mut platform); if self.needs_prepare_entry_call || vm.call_stack_is_empty() { vm.prepare_boot_call(); @@ -182,7 +185,7 @@ impl VirtualMachineRuntime { if budget > 0 { let run_result = { - let mut ctx = HostContext::new(Some(hw)); + let mut ctx = HostContext::new_platform(Some(&mut platform)); let mut host = VmRuntimeHost { runtime: self, log_service, @@ -250,7 +253,7 @@ impl VirtualMachineRuntime { self.current_app_id, ); if self.current_cartridge_app_mode == AppMode::Game { - let mut packet = hw.close_game2d_packet(); + let mut packet = platform.game2d_frame_composer().close_game2d_packet(); packet.gfx2d = std::mem::take(&mut self.gfx2d_commands); self.render_manager .close_frame_with_packet(RenderSubmissionPacket::Game2D(packet)) @@ -264,8 +267,8 @@ impl VirtualMachineRuntime { } let render_outcome = catch_unwind(AssertUnwindSafe(|| { - let mut sink = LegacyHardwareRenderSubmissionSink::new(hw); - let mut surface = HardwareRenderSurface { sink: &mut sink }; + let mut surface = + HardwareRenderSurface { sink: platform.render_submission_sink() }; match self .render_manager .active_render_policy() @@ -307,7 +310,8 @@ impl VirtualMachineRuntime { } // 1. Snapshot full telemetry at logical frame end - let (glyph_bank, sound_bank, scene_bank) = Self::bank_telemetry_summary(hw); + let (glyph_bank, sound_bank, scene_bank) = + Self::bank_telemetry_summary(&platform); self.atomic_telemetry .glyph_slots_used .store(glyph_bank.used_slots as u32, Ordering::Relaxed); @@ -398,7 +402,7 @@ impl VirtualMachineRuntime { // 2. High-frequency telemetry update (only if inspection is active) if self.inspection_active { - let (glyph_bank, sound_bank, scene_bank) = Self::bank_telemetry_summary(hw); + let (glyph_bank, sound_bank, scene_bank) = Self::bank_telemetry_summary(&platform); self.atomic_telemetry .glyph_slots_used .store(glyph_bank.used_slots as u32, Ordering::Relaxed); @@ -435,10 +439,10 @@ impl VirtualMachineRuntime { pub(crate) fn begin_logical_frame( &mut self, _signals: &InputSignals, - hw: &mut dyn HardwareBridge, + platform: &mut dyn RuntimePlatform, ) { - hw.begin_frame(); - hw.audio_mut().clear_commands(); + platform.game2d_frame_composer().begin_frame(); + platform.audio_mut().clear_commands(); self.logs_written_this_frame.clear(); } } From 1d6b799960f407c6dd260341857b4da320b0484a Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 05:59:06 +0100 Subject: [PATCH 24/47] implements PLN-0105 --- .../src/firmware/firmware.rs | 34 +++++++------- .../firmware/firmware_step_crash_screen.rs | 12 +++-- .../firmware/firmware_step_game_running.rs | 2 +- .../src/firmware/firmware_step_hub_home.rs | 2 +- .../firmware/firmware_step_load_cartridge.rs | 2 +- .../firmware/firmware_step_shell_running.rs | 2 +- .../firmware/firmware_step_splash_screen.rs | 16 +++---- .../src/firmware/prometeu_context.rs | 4 +- crates/console/prometeu-hal/src/platform.rs | 4 ++ .../prometeu-system/src/os/facades/vm.rs | 6 +-- .../src/programs/prometeu_hub/prometeu_hub.rs | 44 +++++++++++-------- .../src/services/vm_runtime/tick.rs | 12 +++-- 12 files changed, 73 insertions(+), 67 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index f7e4a1c8..d18531aa 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -3,7 +3,7 @@ use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetSte use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::telemetry::CertificationConfig; -use prometeu_hal::{HardwareBridge, InputSignals}; +use prometeu_hal::{InputSignals, RuntimePlatform}; use prometeu_system::{PrometeuHub, SystemOS}; use prometeu_vm::VirtualMachine; @@ -54,24 +54,24 @@ impl Firmware { /// /// This method is called exactly once per Host frame (60Hz). /// It updates peripheral signals and delegates the logic to the current state. - pub fn tick(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) { + pub fn tick(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) { // 0. Process asset commits at the beginning of the frame boundary. - hw.assets_mut().apply_commits(); + platform.assets_mut().apply_commits(); // 1. Update the peripheral state using the latest signals from the Host. // This ensures input is consistent throughout the entire update. - hw.pad_mut().begin_frame(signals); - hw.touch_mut().begin_frame(signals); + platform.input_mut().pad_mut().begin_frame(signals); + platform.input_mut().touch_mut().begin_frame(signals); // 2. State machine lifecycle management. if !self.state_initialized { - self.on_enter(signals, hw); + self.on_enter(signals, platform); self.state_initialized = true; } // 3. Update the current state and check for transitions. - if let Some(next_state) = self.on_update(signals, hw) { - self.change_state(next_state, signals, hw); + if let Some(next_state) = self.on_update(signals, platform) { + self.change_state(next_state, signals, platform); } } @@ -80,26 +80,26 @@ impl Firmware { &mut self, new_state: FirmwareState, signals: &InputSignals, - hw: &mut dyn HardwareBridge, + platform: &mut dyn RuntimePlatform, ) { - self.on_exit(signals, hw); + self.on_exit(signals, platform); self.state = new_state; self.state_initialized = false; // Enter the new state immediately to avoid "empty" frames during transitions. - self.on_enter(signals, hw); + self.on_enter(signals, platform); self.state_initialized = true; } /// Dispatches the `on_enter` event to the current state implementation. - fn on_enter(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) { + fn on_enter(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) { let mut req = PrometeuContext { vm: &mut self.vm, os: &mut self.os, hub: &mut self.hub, boot_target: &self.boot_target, signals, - hw, + platform, }; match &mut self.state { FirmwareState::Reset(s) => s.on_enter(&mut req), @@ -118,7 +118,7 @@ impl Firmware { fn on_update( &mut self, signals: &InputSignals, - hw: &mut dyn HardwareBridge, + platform: &mut dyn RuntimePlatform, ) -> Option { let mut req = PrometeuContext { vm: &mut self.vm, @@ -126,7 +126,7 @@ impl Firmware { hub: &mut self.hub, boot_target: &self.boot_target, signals, - hw, + platform, }; match &mut self.state { FirmwareState::Reset(s) => s.on_update(&mut req), @@ -141,14 +141,14 @@ impl Firmware { } /// Dispatches the `on_exit` event to the current state implementation. - fn on_exit(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) { + fn on_exit(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) { let mut req = PrometeuContext { vm: &mut self.vm, os: &mut self.os, hub: &mut self.hub, boot_target: &self.boot_target, signals, - hw, + platform, }; match &mut self.state { FirmwareState::Reset(s) => s.on_exit(&mut req), diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs index 7bbf14ac..be2f6d21 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs @@ -2,10 +2,7 @@ use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep}; use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; -use prometeu_hal::{ - FrameId, GfxUiCommand, LegacyHardwareRenderSubmissionSink, RenderSubmission, - RenderSubmissionSink, ShellUiFramePacket, -}; +use prometeu_hal::{FrameId, GfxUiCommand, RenderSubmission, ShellUiFramePacket}; use prometeu_system::CrashReport; #[derive(Debug, Clone)] @@ -24,15 +21,16 @@ impl AppCrashesStep { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { // Update peripherals for input on the crash screen - ctx.hw.pad_mut().begin_frame(ctx.signals); + ctx.platform.input_mut().pad_mut().begin_frame(ctx.signals); let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]); - LegacyHardwareRenderSubmissionSink::new(ctx.hw) + ctx.platform + .render_submission_sink() .submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet)) .expect("firmware crash render submission should publish"); // If START is pressed, return to the Hub - if ctx.hw.pad().start().down { + if ctx.platform.input().pad().start().down { return Some(FirmwareState::LaunchHub(LaunchHubStep)); } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index 5a286f63..911e978b 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs @@ -36,7 +36,7 @@ impl GameRunningStep { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } - let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.hw); + let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.platform); if let Some(report) = result { let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs index a8a0cc8c..ac909263 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs @@ -16,7 +16,7 @@ impl HubHomeStep { } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { - let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.hw); + let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform); if let Some(report) = outcome.crash { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs index e8b1351e..bb26adfd 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs @@ -30,7 +30,7 @@ impl LoadCartridgeStep { ); // Initialize Asset Manager - ctx.hw.assets_mut().initialize_for_cartridge( + ctx.platform.assets_mut().initialize_for_cartridge( self.cartridge.asset_table.clone(), self.cartridge.preload.clone(), self.cartridge.assets.clone(), diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs index f471823e..b0a76f1c 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs @@ -59,7 +59,7 @@ impl ShellRunningStep { } } - let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.hw); + let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform); if let Some(report) = outcome.crash { let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs index 0e49e7d5..f876c91a 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs @@ -3,10 +3,7 @@ use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::primitives::Rect; -use prometeu_hal::{ - FrameId, GfxUiCommand, LegacyHardwareRenderSubmissionSink, RenderSubmission, - RenderSubmissionSink, ShellUiFramePacket, -}; +use prometeu_hal::{FrameId, GfxUiCommand, RenderSubmission, ShellUiFramePacket}; #[derive(Debug, Clone)] pub struct SplashScreenStep { @@ -17,7 +14,7 @@ impl SplashScreenStep { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Showing SplashScreen".to_string()); // Play sound on enter - // ctx.hw.audio_mut().play(0, 0, 0, 255, 127, 1.0, 0, LoopMode::Off); + // ctx.platform.audio_mut().play(0, 0, 0, 255, 127, 1.0, 0, LoopMode::Off); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { @@ -25,10 +22,10 @@ impl SplashScreenStep { const TOTAL_DURATION: u32 = 240; // 4 seconds total (updated from 2s based on total_duration logic) // Update peripherals for input - ctx.hw.pad_mut().begin_frame(ctx.signals); + ctx.platform.input_mut().pad_mut().begin_frame(ctx.signals); // Draw expanding square - let (sw, sh) = ctx.hw.gfx().size(); + let (sw, sh) = ctx.platform.display_size(); let max_size = (sw.min(sh) as i32 / 2).max(1); let current_size = if self.frame < ANIMATION_DURATION { @@ -47,13 +44,14 @@ impl SplashScreenStep { color: Color::WHITE, }, ]); - LegacyHardwareRenderSubmissionSink::new(ctx.hw) + ctx.platform + .render_submission_sink() .submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet)) .expect("firmware splash render submission should publish"); // Transition logic // If any button is pressed at any time after the animation ends - if self.frame >= ANIMATION_DURATION && ctx.hw.pad().any() { + if self.frame >= ANIMATION_DURATION && ctx.platform.input().pad().any() { return Some(FirmwareState::LaunchHub(LaunchHubStep)); } diff --git a/crates/console/prometeu-firmware/src/firmware/prometeu_context.rs b/crates/console/prometeu-firmware/src/firmware/prometeu_context.rs index b3412092..9c37beed 100644 --- a/crates/console/prometeu-firmware/src/firmware/prometeu_context.rs +++ b/crates/console/prometeu-firmware/src/firmware/prometeu_context.rs @@ -1,5 +1,5 @@ use crate::firmware::boot_target::BootTarget; -use prometeu_hal::{HardwareBridge, InputSignals}; +use prometeu_hal::{InputSignals, RuntimePlatform}; use prometeu_system::{PrometeuHub, SystemOS}; use prometeu_vm::VirtualMachine; @@ -9,5 +9,5 @@ pub struct PrometeuContext<'a> { pub hub: &'a mut PrometeuHub, pub boot_target: &'a BootTarget, pub signals: &'a InputSignals, - pub hw: &'a mut dyn HardwareBridge, + pub platform: &'a mut dyn RuntimePlatform, } diff --git a/crates/console/prometeu-hal/src/platform.rs b/crates/console/prometeu-hal/src/platform.rs index 68cc42b0..94b97292 100644 --- a/crates/console/prometeu-hal/src/platform.rs +++ b/crates/console/prometeu-hal/src/platform.rs @@ -215,6 +215,10 @@ impl TelemetryPlatform for NoopTelemetryPlatform {} static LEGACY_NOOP_TELEMETRY: NoopTelemetryPlatform = NoopTelemetryPlatform; pub trait RuntimePlatform { + fn display_size(&self) -> (usize, usize) { + (480, 270) + } + fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink; fn render_backend(&mut self) -> &mut dyn RenderBackend; fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer; diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index 1cf284b3..4786f5ce 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -3,7 +3,7 @@ use crate::os::SystemOS; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; -use prometeu_hal::{HardwareBridge, InputSignals}; +use prometeu_hal::{HardwareBridge, InputSignals, RuntimePlatform}; use prometeu_vm::VirtualMachine; use std::sync::atomic::Ordering; @@ -25,7 +25,7 @@ impl<'a> VmFacade<'a> { &mut self, vm: &mut VirtualMachine, signals: &InputSignals, - hw: &mut dyn HardwareBridge, + platform: &mut dyn RuntimePlatform, ) -> Option { self.os.vm_runtime.tick( &mut self.os.log_service, @@ -36,7 +36,7 @@ impl<'a> VmFacade<'a> { &mut self.os.next_handle, vm, signals, - hw, + platform, ) } diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index d10f2db8..947a8551 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -2,8 +2,8 @@ use crate::{CrashReport, SystemOS}; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; use prometeu_hal::{ - FrameId, GfxUiCommand, HardwareBridge, InputSignals, LegacyHardwareRenderSubmissionSink, - RenderSubmission, RenderSubmissionSink, ShellUiFramePacket, + FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform, + ShellUiFramePacket, }; use prometeu_vm::VirtualMachine; @@ -101,31 +101,35 @@ impl PrometeuHub { pub fn gui_update( &mut self, os: &mut SystemOS, - hw: &mut dyn HardwareBridge, + platform: &mut dyn RuntimePlatform, ) -> Option { let in_shell = os.windows().focused_window().is_some(); if !in_shell { if !self.home_input_armed { - self.home_input_armed = !activation_input_down(hw); + self.home_input_armed = !activation_input_down(platform.input()); return None; } } else { self.home_input_armed = false; } - if let Some(action) = - action_for_click(in_shell, hw.touch().x(), hw.touch().y(), hw.touch().f().pressed) - { + let input = platform.input(); + if let Some(action) = action_for_click( + in_shell, + input.touch().x(), + input.touch().y(), + input.touch().f().pressed, + ) { return Some(action); } None } - pub fn render(&mut self, os: &mut SystemOS, hw: &mut dyn HardwareBridge) { - let pointer_x = hw.touch().x(); - let pointer_y = hw.touch().y(); + pub fn render(&mut self, os: &mut SystemOS, platform: &mut dyn RuntimePlatform) { + let pointer_x = platform.input().touch().x(); + let pointer_y = platform.input().touch().y(); let packet = if os.windows().window_count() == 0 { render_home_packet(pointer_x, pointer_y) @@ -143,7 +147,8 @@ impl PrometeuHub { } ShellUiFramePacket::new(commands) }; - LegacyHardwareRenderSubmissionSink::new(hw) + platform + .render_submission_sink() .submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet)) .expect("hub render submission should publish"); } @@ -153,20 +158,20 @@ impl PrometeuHub { os: &mut SystemOS, vm: &mut VirtualMachine, signals: &InputSignals, - hw: &mut dyn HardwareBridge, + platform: &mut dyn RuntimePlatform, ) -> SystemProfileUpdate { let mut crash = None; - let mut action = self.gui_update(os, hw); + let mut action = self.gui_update(os, platform); if os.windows().focused_window().is_some() { - if hw.pad().start().down { + if platform.input().pad().start().down { action = Some(SystemProfileAction::CloseShell); } else if action != Some(SystemProfileAction::CloseShell) { - crash = os.vm().tick(vm, signals, hw); + crash = os.vm().tick(vm, signals, platform); } } - self.render(os, hw); + self.render(os, platform); SystemProfileUpdate { crash, action } } @@ -191,8 +196,11 @@ fn rect_contains(rect: Rect, x: i32, y: i32) -> bool { x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h } -fn activation_input_down(hw: &dyn HardwareBridge) -> bool { - hw.touch().f().down || hw.pad().a().down || hw.pad().b().down || hw.pad().start().down +fn activation_input_down(input: &dyn InputPlatform) -> bool { + input.touch().f().down + || input.pad().a().down + || input.pad().b().down + || input.pad().start().down } fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket { diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index 0aac4cb7..267852da 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -135,7 +135,7 @@ impl VirtualMachineRuntime { next_handle: &mut u32, vm: &mut VirtualMachine, signals: &InputSignals, - hw: &mut dyn HardwareBridge, + platform: &mut dyn RuntimePlatform, ) -> Option { let start = Instant::now(); self.tick_index += 1; @@ -145,8 +145,6 @@ impl VirtualMachineRuntime { } self.update_fs(log_service, fs, fs_state); - let mut platform = LegacyHardwareRuntimePlatform::new(hw); - if !self.logical_frame_active { if RenderManager::policy_for_app_mode(self.current_cartridge_app_mode) .uses_frame_scheduler() @@ -157,7 +155,7 @@ impl VirtualMachineRuntime { } self.logical_frame_active = true; self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME; - self.begin_logical_frame(signals, &mut platform); + self.begin_logical_frame(signals, platform); if self.needs_prepare_entry_call || vm.call_stack_is_empty() { vm.prepare_boot_call(); @@ -185,7 +183,7 @@ impl VirtualMachineRuntime { if budget > 0 { let run_result = { - let mut ctx = HostContext::new_platform(Some(&mut platform)); + let mut ctx = HostContext::new_platform(Some(platform)); let mut host = VmRuntimeHost { runtime: self, log_service, @@ -311,7 +309,7 @@ impl VirtualMachineRuntime { // 1. Snapshot full telemetry at logical frame end let (glyph_bank, sound_bank, scene_bank) = - Self::bank_telemetry_summary(&platform); + Self::bank_telemetry_summary(platform); self.atomic_telemetry .glyph_slots_used .store(glyph_bank.used_slots as u32, Ordering::Relaxed); @@ -402,7 +400,7 @@ impl VirtualMachineRuntime { // 2. High-frequency telemetry update (only if inspection is active) if self.inspection_active { - let (glyph_bank, sound_bank, scene_bank) = Self::bank_telemetry_summary(&platform); + let (glyph_bank, sound_bank, scene_bank) = Self::bank_telemetry_summary(platform); self.atomic_telemetry .glyph_slots_used .store(glyph_bank.used_slots as u32, Ordering::Relaxed); From 5381ae02b1c7878ef30f0662f01c3e41197c8da3 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 06:00:39 +0100 Subject: [PATCH 25/47] implements PLN-0106 --- .../prometeu-host-desktop-winit/src/input.rs | 32 ++++++++++++------- .../prometeu-host-desktop-winit/src/runner.rs | 20 +++++++----- .../prometeu-host-desktop-winit/src/stats.rs | 9 +----- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/crates/host/prometeu-host-desktop-winit/src/input.rs b/crates/host/prometeu-host-desktop-winit/src/input.rs index ea58835e..7d9874ef 100644 --- a/crates/host/prometeu-host-desktop-winit/src/input.rs +++ b/crates/host/prometeu-host-desktop-winit/src/input.rs @@ -1,4 +1,3 @@ -use prometeu_drivers::hardware::Hardware; use prometeu_hal::InputSignals; use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::keyboard::{KeyCode, PhysicalKey}; @@ -14,7 +13,7 @@ struct FbViewport { } impl FbViewport { - fn framebuffer_viewport(window: &Window) -> Option { + fn framebuffer_viewport(window: &Window, display_size: (usize, usize)) -> Option { let size = window.inner_size(); let win_w = size.width as f32; @@ -24,8 +23,8 @@ impl FbViewport { return None; } - let fb_w = Hardware::W as f32; - let fb_h = Hardware::H as f32; + let fb_w = display_size.0 as f32; + let fb_h = display_size.1 as f32; let scale_x = (win_w / fb_w).floor(); let scale_y = (win_h / fb_h).floor(); @@ -44,17 +43,18 @@ impl FbViewport { pub struct HostInputHandler { pub signals: InputSignals, + display_size: (usize, usize), } impl Default for HostInputHandler { fn default() -> Self { - Self::new() + Self::new((480, 270)) } } impl HostInputHandler { - pub fn new() -> Self { - Self { signals: InputSignals::default() } + pub fn new(display_size: (usize, usize)) -> Self { + Self { signals: InputSignals::default(), display_size } } pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) { @@ -87,7 +87,9 @@ impl HostInputHandler { } WindowEvent::CursorMoved { position, .. } => { - if let Some((x, y)) = window_to_fb(position.x as f32, position.y as f32, window) { + if let Some((x, y)) = + window_to_fb(position.x as f32, position.y as f32, window, self.display_size) + { self.signals.x_pos = x; self.signals.y_pos = y; } @@ -107,8 +109,13 @@ impl HostInputHandler { _ => {} } - fn window_to_fb(wx: f32, wy: f32, window: &Window) -> Option<(i32, i32)> { - let viewport = FbViewport::framebuffer_viewport(window)?; + fn window_to_fb( + wx: f32, + wy: f32, + window: &Window, + display_size: (usize, usize), + ) -> Option<(i32, i32)> { + let viewport = FbViewport::framebuffer_viewport(window, display_size)?; let local_x = wx - viewport.x; let local_y = wy - viewport.y; @@ -120,7 +127,10 @@ impl HostInputHandler { let fb_x = (local_x / viewport.scale).floor() as i32; let fb_y = (local_y / viewport.scale).floor() as i32; - Some((fb_x.clamp(0, Hardware::W as i32 - 1), fb_y.clamp(0, Hardware::H as i32 - 1))) + Some(( + fb_x.clamp(0, display_size.0 as i32 - 1), + fb_y.clamp(0, display_size.1 as i32 - 1), + )) } } } diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index 035d251b..53235b3d 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -10,6 +10,7 @@ use pixels::{Pixels, PixelsBuilder, SurfaceTexture}; use prometeu_drivers::AudioCommand; use prometeu_drivers::hardware::Hardware; use prometeu_firmware::{BootTarget, Firmware, FirmwareState}; +use prometeu_hal::RuntimePlatform; use prometeu_hal::telemetry::CertificationConfig; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; @@ -152,12 +153,15 @@ impl HostRunner { firmware.os.fs().mount(Box::new(backend)); } + let hardware = Hardware::new(); + let display_size = hardware.display_size(); + Self { window: None, pixels: None, - hardware: Hardware::new(), + hardware, firmware, - input: HostInputHandler::new(), + input: HostInputHandler::new(display_size), fs_root, log_sink: HostConsoleSink::new(), frame_target_dt: Duration::from_nanos(1_000_000_000 / target_fps), @@ -228,11 +232,11 @@ impl ApplicationHandler for HostRunner { let size = window.inner_size(); let surface_texture = SurfaceTexture::new(size.width, size.height, window); - let mut pixels = - PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture) - .present_mode(PresentMode::Fifo) // activate vsync - .build() - .expect("failed to create Pixels"); + let (display_w, display_h) = self.hardware.display_size(); + let mut pixels = PixelsBuilder::new(display_w as u32, display_h as u32, surface_texture) + .present_mode(PresentMode::Fifo) // activate vsync + .build() + .expect("failed to create Pixels"); pixels.frame_mut().fill(0); @@ -381,7 +385,7 @@ impl ApplicationHandler for HostRunner { self.audio.update_stats(&mut self.stats); // Update technical statistics displayed in the window title. - self.stats.update(now, self.window, &self.hardware, &mut self.firmware); + self.stats.update(now, self.window, &mut self.firmware); // Synchronize system logs to the host console. let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX); diff --git a/crates/host/prometeu-host-desktop-winit/src/stats.rs b/crates/host/prometeu-host-desktop-winit/src/stats.rs index fb20f9c7..966879d7 100644 --- a/crates/host/prometeu-host-desktop-winit/src/stats.rs +++ b/crates/host/prometeu-host-desktop-winit/src/stats.rs @@ -1,4 +1,3 @@ -use prometeu_drivers::hardware::Hardware; use prometeu_firmware::Firmware; use std::time::{Duration, Instant}; use winit::window::Window; @@ -60,13 +59,7 @@ impl HostStats { } } - pub fn update( - &mut self, - now: Instant, - window: Option<&Window>, - _hardware: &Hardware, - firmware: &mut Firmware, - ) { + pub fn update(&mut self, now: Instant, window: Option<&Window>, firmware: &mut Firmware) { let stats_elapsed = now.duration_since(self.last_stats_update); if stats_elapsed >= Duration::from_secs(1) { self.current_fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64(); From f45b3a38cb65e743860efb4c9767703a0e175dd1 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 06:01:49 +0100 Subject: [PATCH 26/47] implements PLN-0107 --- crates/console/prometeu-vm/src/builtins.rs | 40 ++++++++++++---------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/crates/console/prometeu-vm/src/builtins.rs b/crates/console/prometeu-vm/src/builtins.rs index 80c056da..5bafab6f 100644 --- a/crates/console/prometeu-vm/src/builtins.rs +++ b/crates/console/prometeu-vm/src/builtins.rs @@ -1350,8 +1350,9 @@ fn input_touch_x( ctx: &mut HostContext<'_>, ) -> Result, IntrinsicExecutionError> { expect_touch_handle(args, "x")?; - let hw = ctx.require_hw().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?; - Ok(vec![Value::Int64(hw.touch().x() as i64)]) + let platform = + ctx.require_platform().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?; + Ok(vec![Value::Int64(platform.input().touch().x() as i64)]) } fn input_touch_y( @@ -1359,8 +1360,9 @@ fn input_touch_y( ctx: &mut HostContext<'_>, ) -> Result, IntrinsicExecutionError> { expect_touch_handle(args, "y")?; - let hw = ctx.require_hw().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?; - Ok(vec![Value::Int64(hw.touch().y() as i64)]) + let platform = + ctx.require_platform().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?; + Ok(vec![Value::Int64(platform.input().touch().y() as i64)]) } fn input_button_pressed( @@ -1435,9 +1437,11 @@ fn resolve_button( ctx: &mut HostContext<'_>, ) -> Result { let carrier = expect_builtin_carrier(args, 0)?; - let hw = ctx.require_hw().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?; + let platform = + ctx.require_platform().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?; + let input = platform.input(); if carrier == TOUCH_BUTTON_CARRIER { - return Ok(*hw.touch().f()); + return Ok(*input.touch().f()); } let Some(index) = carrier.checked_sub(PAD_BUTTON_BASE) else { @@ -1448,18 +1452,18 @@ fn resolve_button( }); }; let button = match index { - 0 => *hw.pad().up(), - 1 => *hw.pad().down(), - 2 => *hw.pad().left(), - 3 => *hw.pad().right(), - 4 => *hw.pad().a(), - 5 => *hw.pad().b(), - 6 => *hw.pad().x(), - 7 => *hw.pad().y(), - 8 => *hw.pad().l(), - 9 => *hw.pad().r(), - 10 => *hw.pad().start(), - 11 => *hw.pad().select(), + 0 => *input.pad().up(), + 1 => *input.pad().down(), + 2 => *input.pad().left(), + 3 => *input.pad().right(), + 4 => *input.pad().a(), + 5 => *input.pad().b(), + 6 => *input.pad().x(), + 7 => *input.pad().y(), + 8 => *input.pad().l(), + 9 => *input.pad().r(), + 10 => *input.pad().start(), + 11 => *input.pad().select(), _ => { return Err(IntrinsicExecutionError::InvalidBuiltinCarrier { owner: "input.button", From 117a18ee7266d4b29761ee1aa7c094b6519e2711 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 06:04:29 +0100 Subject: [PATCH 27/47] implements PLN-0108 --- .../src/firmware/firmware.rs | 86 ++++++------ .../src/services/vm_runtime/tests.rs | 132 +++++++++--------- .../services/vm_runtime/tests_asset_bank.rs | 58 ++++---- .../services/vm_runtime/tests_fs_memcard.rs | 8 +- .../src/debugger.rs | 18 ++- 5 files changed, 155 insertions(+), 147 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index d18531aa..eeba7e41 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -174,7 +174,7 @@ mod tests { use crate::firmware::firmware_state::HubHomeStep; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; - use prometeu_drivers::hardware::Hardware; + use prometeu_drivers::TestPlatform; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::color::Color; @@ -267,28 +267,28 @@ mod tests { } } - fn load_shell_running_firmware() -> (Firmware, Hardware, InputSignals, TaskId) { + fn load_shell_running_firmware() -> (Firmware, TestPlatform, InputSignals, TaskId) { let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Shell)); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, other => panic!("expected ShellRunning state, got {:?}", other), }; - (firmware, hardware, signals, task_id) + (firmware, platform, signals, task_id) } - fn hub_home_firmware() -> (Firmware, Hardware) { + fn hub_home_firmware() -> (Firmware, TestPlatform) { let mut firmware = Firmware::new(None); firmware.state = FirmwareState::HubHome(HubHomeStep); firmware.state_initialized = false; - (firmware, Hardware::new()) + (firmware, TestPlatform::new()) } fn focus_window(firmware: &mut Firmware, owner: WindowOwner) { @@ -306,11 +306,11 @@ mod tests { #[test] fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() { let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(invalid_game_cartridge()); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); match &firmware.state { FirmwareState::AppCrashes(step) => match &step.report { @@ -326,18 +326,18 @@ mod tests { #[test] fn game_running_transitions_to_app_crashes_when_runtime_surfaces_trap() { let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(trapping_game_cartridge()); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::GameRunning(step) => step.task_id, other => panic!("expected GameRunning state, got {:?}", other), }; - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); match &firmware.state { FirmwareState::AppCrashes(step) => match &step.report { @@ -359,11 +359,11 @@ mod tests { #[test] fn reset_routes_hub_boot_target_to_splash_screen() { let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.boot_target = BootTarget::Hub; - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::SplashScreen(_))); } @@ -371,12 +371,12 @@ mod tests { #[test] fn reset_routes_cartridge_boot_target_to_launch_hub() { let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.boot_target = BootTarget::Cartridge { path: "missing-cart".into(), debug: false, debug_port: 7777 }; - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::LaunchHub(_))); } @@ -384,11 +384,11 @@ mod tests { #[test] fn load_cartridge_routes_system_apps_to_system_running() { let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Shell)); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::ShellRunning(_))); assert!(firmware.os.windows().focused_window().is_some()); @@ -398,18 +398,18 @@ mod tests { #[test] fn system_running_ticks_focused_system_app_through_hub_pipeline() { let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Shell)); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, other => panic!("expected ShellRunning state, got {:?}", other), }; let tick_index_before_system_update = firmware.os.vm().tick_index(); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::ShellRunning(_))); assert!(firmware.os.windows().focused_window_belongs_to_task(task_id)); @@ -418,10 +418,10 @@ mod tests { #[test] fn hub_home_does_not_launch_shell_before_home_input_is_armed() { - let (mut firmware, mut hardware) = hub_home_firmware(); + let (mut firmware, mut platform) = hub_home_firmware(); let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 108, ..Default::default() }; - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.windows().window_count(), 0); @@ -429,11 +429,11 @@ mod tests { #[test] fn hub_home_click_launches_shell_a_as_native_shell_task_window() { - let (mut firmware, mut hardware) = hub_home_firmware(); - firmware.tick(&InputSignals::default(), &mut hardware); + let (mut firmware, mut platform) = hub_home_firmware(); + firmware.tick(&InputSignals::default(), &mut platform); let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 108, ..Default::default() }; - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, @@ -452,11 +452,11 @@ mod tests { #[test] fn hub_home_click_launches_shell_b_as_native_shell_task_window() { - let (mut firmware, mut hardware) = hub_home_firmware(); - firmware.tick(&InputSignals::default(), &mut hardware); + let (mut firmware, mut platform) = hub_home_firmware(); + firmware.tick(&InputSignals::default(), &mut platform); let signals = InputSignals { f_signal: true, x_pos: 256, y_pos: 108, ..Default::default() }; - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, @@ -475,10 +475,10 @@ mod tests { #[test] fn shell_running_start_close_uses_lifecycle_and_returns_hub_home() { - let (mut firmware, mut hardware, _signals, task_id) = load_shell_running_firmware(); + let (mut firmware, mut platform, _signals, task_id) = load_shell_running_firmware(); let close_signals = InputSignals { start_signal: true, ..Default::default() }; - firmware.tick(&close_signals, &mut hardware); + firmware.tick(&close_signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); @@ -490,11 +490,11 @@ mod tests { #[test] fn shell_running_close_click_uses_lifecycle_and_returns_hub_home() { - let (mut firmware, mut hardware, _signals, task_id) = load_shell_running_firmware(); + let (mut firmware, mut platform, _signals, task_id) = load_shell_running_firmware(); let close_signals = InputSignals { f_signal: true, x_pos: 420, y_pos: 24, ..Default::default() }; - firmware.tick(&close_signals, &mut hardware); + firmware.tick(&close_signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); @@ -506,10 +506,10 @@ mod tests { #[test] fn shell_running_closes_task_and_returns_hub_home_without_focused_window() { - let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware(); + let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware(); firmware.os.windows().remove_all_windows(); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); @@ -521,10 +521,10 @@ mod tests { #[test] fn shell_running_closes_task_and_returns_hub_home_for_focused_hub_window() { - let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware(); + let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware(); focus_window(&mut firmware, WindowOwner::Hub); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); @@ -536,10 +536,10 @@ mod tests { #[test] fn shell_running_closes_task_and_returns_hub_home_for_focused_overlay_window() { - let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware(); + let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware(); focus_window(&mut firmware, WindowOwner::Overlay); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); @@ -551,10 +551,10 @@ mod tests { #[test] fn shell_running_closes_task_and_returns_hub_home_for_other_task_window() { - let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware(); + let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware(); focus_window(&mut firmware, WindowOwner::Task(TaskId(task_id.0 + 1))); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); @@ -567,11 +567,11 @@ mod tests { #[test] fn load_cartridge_routes_game_apps_to_game_running() { let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Game)); - firmware.tick(&signals, &mut hardware); + firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); assert_eq!(firmware.os.windows().window_count(), 0); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs index 4fa6e7b7..a10b47c1 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs @@ -5,11 +5,10 @@ use prometeu_bytecode::TRAP_TYPE; use prometeu_bytecode::Value; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl}; -use prometeu_drivers::hardware::Hardware; +use prometeu_drivers::TestPlatform; use prometeu_drivers::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller}; use prometeu_hal::AudioOpStatus; use prometeu_hal::ComposerOpStatus; -use prometeu_hal::InputSignals; use prometeu_hal::asset::{ AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus, }; @@ -22,6 +21,7 @@ use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::syscalls::caps; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; +use prometeu_hal::{InputSignals, RuntimePlatform}; use prometeu_vm::VmInitError; use std::collections::HashMap; use std::sync::Arc; @@ -247,7 +247,7 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -274,7 +274,7 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ) .expect("trap must surface as runtime error"); @@ -298,7 +298,7 @@ fn tick_system_profile_rejects_gfx_game_surface() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -324,7 +324,7 @@ fn tick_system_profile_rejects_gfx_game_surface() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ) .expect("system gfx call must trap"); @@ -347,7 +347,7 @@ fn tick_system_profile_rejects_composer_game_surface() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -373,7 +373,7 @@ fn tick_system_profile_rejects_composer_game_surface() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ) .expect("system composer call must trap"); @@ -398,7 +398,7 @@ fn tick_game_profile_rejects_gfxui_shell_surface() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -424,7 +424,7 @@ fn tick_game_profile_rejects_gfxui_shell_surface() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ) .expect("game gfxui call must trap"); @@ -447,7 +447,7 @@ fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 287454020\nHOSTCALL 0\nFRAME_SYNC\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -472,7 +472,7 @@ fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none()); @@ -483,7 +483,7 @@ fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() { }; assert_eq!(packet.commands.len(), 1); assert!(matches!(packet.commands[0], prometeu_hal::GfxUiCommand::Clear { .. })); - assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); + assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); assert_eq!(runtime.frame_scheduler.completed_game_frames(), 0); assert_eq!(runtime.frame_scheduler.active_game_frame_id(), None); } @@ -498,7 +498,7 @@ fn tick_gfx2d_clear_buffers_without_immediate_pixel_mutation_before_frame_close( let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 287454020\nHOSTCALL 0\nJMP 0").expect("assemble"); let program = serialized_single_function_module( @@ -523,7 +523,7 @@ fn tick_gfx2d_clear_buffers_without_immediate_pixel_mutation_before_frame_close( &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none()); @@ -533,7 +533,7 @@ fn tick_gfx2d_clear_buffers_without_immediate_pixel_mutation_before_frame_close( Some(prometeu_hal::Gfx2dCommand::Clear { color }) if *color == Color::from_raw(0x11223344) )); - assert_ne!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); + assert_ne!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); } #[test] @@ -546,7 +546,7 @@ fn tick_gfx2d_draw_text_buffers_without_immediate_pixel_mutation_before_frame_cl let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 287454020\nHOSTCALL 0\nJMP 0") @@ -574,7 +574,7 @@ fn tick_gfx2d_draw_text_buffers_without_immediate_pixel_mutation_before_frame_cl &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none()); @@ -584,7 +584,7 @@ fn tick_gfx2d_draw_text_buffers_without_immediate_pixel_mutation_before_frame_cl Some(prometeu_hal::Gfx2dCommand::DrawText { x: 0, y: 0, text, color }) if text == "I" && *color == Color::from_raw(0x11223344) )); - assert_ne!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); + assert_ne!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); } #[test] @@ -597,7 +597,7 @@ fn tick_system_profile_rejects_bank_game_surface() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -623,7 +623,7 @@ fn tick_system_profile_rejects_bank_game_surface() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ) .expect("system bank call must trap"); @@ -646,7 +646,7 @@ fn tick_system_profile_can_use_shared_log_transport() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module_with_consts( @@ -672,7 +672,7 @@ fn tick_system_profile_can_use_shared_log_transport() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "system log transport must remain internally shared"); @@ -689,7 +689,7 @@ fn tick_returns_panic_report_distinct_from_trap() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let report = runtime @@ -702,7 +702,7 @@ fn tick_returns_panic_report_distinct_from_trap() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ) .expect("panic must surface as runtime error"); @@ -734,12 +734,12 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE))); banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8))); - let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks)); - let glyph_slot_index = hardware.assets.glyph_asset_slot_index(); + let mut platform = TestPlatform::new_with_memory_banks(Arc::clone(&banks)); + let glyph_slot_index = platform.local_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)); + assert!(platform.local_hardware_mut().frame_composer.bind_scene(0)); runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick( @@ -751,11 +751,11 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "frame render path must not crash"); - assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); + assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::BLUE.raw()); } #[test] @@ -807,8 +807,8 @@ fn tick_renders_scene_through_public_composer_syscalls() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE))); banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8))); - let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks)); - let glyph_slot_index = hardware.assets.glyph_asset_slot_index(); + let mut platform = TestPlatform::new_with_memory_banks(Arc::clone(&banks)); + let glyph_slot_index = platform.local_hardware().assets.glyph_asset_slot_index(); let mut slots = [None; 16]; slots[0] = Some(0); glyph_slot_index.rebuild_from_slots(&slots); @@ -823,12 +823,12 @@ fn tick_renders_scene_through_public_composer_syscalls() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "public composer path must not crash"); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]); - assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); + assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::BLUE.raw()); } #[test] @@ -858,7 +858,7 @@ fn tick_draw_text_survives_no_scene_frame_path() { }], ); let cartridge = cartridge_with_program(program, caps::GFX); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick( @@ -870,7 +870,7 @@ fn tick_draw_text_survives_no_scene_frame_path() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "no-scene overlay text must not crash"); @@ -881,7 +881,7 @@ fn tick_draw_text_survives_no_scene_frame_path() { }; assert_eq!(packet.gfx2d.len(), 1); assert!(matches!(packet.gfx2d[0], prometeu_hal::Gfx2dCommand::DrawText { .. })); - assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); + assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); } #[test] @@ -907,7 +907,7 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() { }], ); let cartridge = cartridge_with_program(program, caps::GFX); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); let first_report = runtime.tick( @@ -919,7 +919,7 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(first_report.is_none()); let first_submission = @@ -933,7 +933,7 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() { first_packet.gfx2d[0], prometeu_hal::Gfx2dCommand::Clear { color } if color == Color::from_raw(0x11223344) )); - assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); + assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); assert_eq!(runtime.frame_scheduler.completed_game_frames(), 1); let second_packet = prometeu_hal::Game2DFramePacket::new( @@ -957,9 +957,11 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() { first_packet.gfx2d[0], prometeu_hal::Gfx2dCommand::Clear { color } if color == Color::from_raw(0x11223344) )); - prometeu_hal::RenderSubmissionSink::submit_render_submission(&mut hardware, latest.clone()) + platform + .render_submission_sink() + .submit_render_submission(latest.clone()) .expect("local sink should publish latest submission"); - assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x55667788).raw()); + assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x55667788).raw()); } #[test] @@ -1061,7 +1063,7 @@ fn tick_numeric_happy_path_records_zero_internal_allocations() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 1\nPUSH_I32 2\nADD\nPOP_N 1\nFRAME_SYNC\nHALT").expect("assemble"); @@ -1078,7 +1080,7 @@ fn tick_numeric_happy_path_records_zero_internal_allocations() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none()); @@ -1097,7 +1099,7 @@ fn tick_already_materialized_string_path_records_zero_internal_allocations() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_CONST 0\nSET_GLOBAL 0\nGET_GLOBAL 0\nPOP_N 1\nFRAME_SYNC\nHALT") .expect("assemble"); @@ -1118,7 +1120,7 @@ fn tick_already_materialized_string_path_records_zero_internal_allocations() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none()); @@ -1193,7 +1195,7 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 99\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -1218,7 +1220,7 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); @@ -1238,7 +1240,7 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", @@ -1266,7 +1268,7 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); @@ -1283,7 +1285,7 @@ fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 4\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", @@ -1311,7 +1313,7 @@ fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "invalid layer must not crash"); assert!(vm.is_halted()); @@ -1328,7 +1330,7 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 64\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", @@ -1347,7 +1349,7 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() { let cartridge = cartridge_with_program(program, caps::GFX); let asset_data = test_glyph_asset_data(); - hardware.assets.initialize_for_cartridge( + platform.local_hardware().assets.initialize_for_cartridge( vec![test_glyph_asset_entry("tile_asset", asset_data.len())], vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }], AssetsPayloadSource::from_bytes(asset_data), @@ -1363,7 +1365,7 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "invalid composer parameter range must not crash"); assert!(vm.is_halted()); @@ -1383,7 +1385,7 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 -1\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nHOSTCALL 0\nHALT", @@ -1411,7 +1413,7 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); @@ -1428,7 +1430,7 @@ fn tick_audio_play_voice_invalid_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 16\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT", @@ -1456,7 +1458,7 @@ fn tick_audio_play_voice_invalid_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "invalid voice must not crash"); assert!(vm.is_halted()); @@ -1473,7 +1475,7 @@ fn tick_audio_play_missing_asset_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT", @@ -1501,7 +1503,7 @@ fn tick_audio_play_missing_asset_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "missing audio asset must not crash"); assert!(vm.is_halted()); @@ -1518,7 +1520,7 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT", @@ -1547,7 +1549,7 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ) .expect("type mismatch must surface as trap"); match report { @@ -1570,7 +1572,7 @@ fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", @@ -1599,7 +1601,7 @@ fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ) .expect("type mismatch must surface as trap"); match report { diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs index 28584be8..ccf91534 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs @@ -10,7 +10,7 @@ fn tick_asset_commit_operational_error_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -35,7 +35,7 @@ fn tick_asset_commit_operational_error_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); @@ -52,7 +52,7 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -77,7 +77,7 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "missing asset must not crash"); assert!(vm.is_halted()); @@ -97,10 +97,10 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let asset_data = test_glyph_asset_data(); - hardware.assets.initialize_for_cartridge( + platform.local_hardware().assets.initialize_for_cartridge( vec![test_glyph_asset_entry("tile_asset", asset_data.len())], vec![], AssetsPayloadSource::from_bytes(asset_data), @@ -128,7 +128,7 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "invalid slot must not crash"); assert!(vm.is_halted()); @@ -148,7 +148,7 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -173,7 +173,7 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "unknown asset handle must not crash"); assert!(vm.is_halted()); @@ -190,10 +190,10 @@ fn tick_bank_info_returns_slot_summary_not_json() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let asset_data = test_glyph_asset_data(); - hardware.assets.initialize_for_cartridge( + platform.local_hardware().assets.initialize_for_cartridge( vec![test_glyph_asset_entry("tile_asset", asset_data.len())], vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }], AssetsPayloadSource::from_bytes(asset_data), @@ -221,7 +221,7 @@ fn tick_bank_info_returns_slot_summary_not_json() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "bank summary must not crash"); assert!(vm.is_halted()); @@ -261,7 +261,7 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 1\nHOSTCALL 0\nPOP_N 1\nPUSH_I32 1\nHOSTCALL 1\nHALT") .expect("assemble"); @@ -287,12 +287,13 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() { let cartridge = cartridge_with_program(program, caps::ASSET); let asset_data = test_glyph_asset_data(); - hardware.assets.initialize_for_cartridge( + platform.local_hardware().assets.initialize_for_cartridge( vec![test_glyph_asset_entry("tile_asset", asset_data.len())], vec![], AssetsPayloadSource::from_bytes(asset_data), ); - let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated"); + let handle = + platform.local_hardware().assets.load(7, 0).expect("asset handle must be allocated"); runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick( @@ -304,7 +305,7 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "invalid transition must not crash"); assert!(vm.is_halted()); @@ -322,7 +323,7 @@ fn tick_asset_cancel_unknown_handle_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -347,7 +348,7 @@ fn tick_asset_cancel_unknown_handle_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "unknown handle cancel must not crash"); assert!(vm.is_halted()); @@ -364,7 +365,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 1\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -380,17 +381,18 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { let cartridge = cartridge_with_program(program, caps::ASSET); let asset_data = test_glyph_asset_data(); - hardware.assets.initialize_for_cartridge( + platform.local_hardware().assets.initialize_for_cartridge( vec![test_glyph_asset_entry("tile_asset", asset_data.len())], vec![], AssetsPayloadSource::from_bytes(asset_data), ); - let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated"); + let handle = + platform.local_hardware().assets.load(7, 0).expect("asset handle must be allocated"); runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); loop { - match hardware.assets.status(handle) { + match platform.local_hardware().assets.status(handle) { LoadStatus::READY => break, LoadStatus::PENDING | LoadStatus::LOADING => { std::thread::sleep(std::time::Duration::from_millis(1)); @@ -399,9 +401,9 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { } } - assert_eq!(hardware.assets.commit(handle), AssetOpStatus::Ok); - hardware.assets.apply_commits(); - assert_eq!(hardware.assets.status(handle), LoadStatus::COMMITTED); + assert_eq!(platform.local_hardware().assets.commit(handle), AssetOpStatus::Ok); + platform.local_hardware().assets.apply_commits(); + assert_eq!(platform.local_hardware().assets.status(handle), LoadStatus::COMMITTED); let report = runtime.tick( &mut log_service, @@ -412,7 +414,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "cancel after commit must not crash"); assert!(vm.is_halted()); @@ -429,7 +431,7 @@ fn tick_status_first_surface_smoke_across_composer_audio_and_asset() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\ @@ -476,7 +478,7 @@ fn tick_status_first_surface_smoke_across_composer_audio_and_asset() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "mixed status-first surface must not crash"); assert!(vm.is_halted()); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests_fs_memcard.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests_fs_memcard.rs index 3d1c80d1..29d185ea 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests_fs_memcard.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests_fs_memcard.rs @@ -11,7 +11,7 @@ fn tick_memcard_slot_roundtrip_for_game_profile() { let mut next_handle = 1; runtime.mount_fs(&mut log_service, &mut fs, &mut fs_state, Box::new(MemFsBackend::default())); let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nHOSTCALL 0\nPOP_N 2\nPUSH_I32 0\nHOSTCALL 1\nPOP_N 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 10\nHOSTCALL 2\nHALT", @@ -66,7 +66,7 @@ fn tick_memcard_slot_roundtrip_for_game_profile() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "memcard roundtrip must not crash"); assert!(vm.is_halted()); @@ -86,7 +86,7 @@ fn tick_memcard_access_is_denied_for_non_game_profile() { let mut open_files: HashMap = HashMap::new(); let mut next_handle = 1; let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); let signals = InputSignals::default(); let code = assemble("HOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -121,7 +121,7 @@ fn tick_memcard_access_is_denied_for_non_game_profile() { &mut next_handle, &mut vm, &signals, - &mut hardware, + &mut platform, ); assert!(report.is_none(), "non-game memcard call must return status"); assert!(vm.is_halted()); diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index c635a4f6..4ebf6291 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -372,7 +372,7 @@ impl HostDebugger { #[cfg(test)] mod tests { use super::*; - use prometeu_drivers::hardware::Hardware; + use prometeu_drivers::TestPlatform; use prometeu_hal::debugger_protocol::DebugCommand; #[test] @@ -396,15 +396,19 @@ mod tests { fn handle_command_updates_pause_and_step_flags_without_host_io() { let mut debugger = HostDebugger::new(); let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); - debugger.handle_command(DebugCommand::Pause, &mut firmware, &mut hardware); + debugger.handle_command(DebugCommand::Pause, &mut firmware, platform.local_hardware_mut()); assert!(firmware.os.vm().paused()); - debugger.handle_command(DebugCommand::Resume, &mut firmware, &mut hardware); + debugger.handle_command(DebugCommand::Resume, &mut firmware, platform.local_hardware_mut()); assert!(!firmware.os.vm().paused()); - debugger.handle_command(DebugCommand::StepFrame, &mut firmware, &mut hardware); + debugger.handle_command( + DebugCommand::StepFrame, + &mut firmware, + platform.local_hardware_mut(), + ); assert!(!firmware.os.vm().paused()); assert!(firmware.os.vm().debug_step_requested()); } @@ -413,12 +417,12 @@ mod tests { fn handle_command_start_leaves_waiting_for_start_mode() { let mut debugger = HostDebugger::new(); let mut firmware = Firmware::new(None); - let mut hardware = Hardware::new(); + let mut platform = TestPlatform::new(); debugger.waiting_for_start = true; firmware.os.vm().set_paused(true); - debugger.handle_command(DebugCommand::Start, &mut firmware, &mut hardware); + debugger.handle_command(DebugCommand::Start, &mut firmware, platform.local_hardware_mut()); assert!(!debugger.waiting_for_start); assert!(!firmware.os.vm().paused()); From 9cf9fa6163d74df69a42d8339656062d6b20089d Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 10:47:34 +0100 Subject: [PATCH 28/47] implements PLN-0109 --- .../console/prometeu-drivers/src/hardware.rs | 105 ++-------- .../prometeu-hal/src/hardware_bridge.rs | 39 ---- .../console/prometeu-hal/src/host_context.rs | 16 +- crates/console/prometeu-hal/src/lib.rs | 7 +- crates/console/prometeu-hal/src/platform.rs | 197 ------------------ .../prometeu-system/src/os/facades/vm.rs | 6 +- .../src/services/vm_runtime/tick.rs | 10 +- .../src/debugger.rs | 10 +- docs/specs/runtime/04-gfx-peripheral.md | 11 + ...ortability-and-cross-platform-execution.md | 4 + docs/specs/runtime/README.md | 6 + 11 files changed, 60 insertions(+), 351 deletions(-) delete mode 100644 crates/console/prometeu-hal/src/hardware_bridge.rs diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 244518e1..6646e461 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -11,8 +11,8 @@ use crate::touch::Touch; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::sprite::Sprite; use prometeu_hal::{ - AssetBridge, AudioBridge, GfxBridge, HardwareBridge, InputPlatform, NoopTelemetryPlatform, - PadBridge, RenderBackend, RuntimePlatform, TelemetryPlatform, TouchBridge, + AssetBridge, AudioBridge, InputPlatform, NoopTelemetryPlatform, PadBridge, RenderBackend, + RuntimePlatform, TelemetryPlatform, TouchBridge, }; use prometeu_hal::{ Game2DFrameComposer, Game2DFramePacket, RenderSubmission, RenderSubmissionPacket, @@ -53,32 +53,8 @@ impl Default for Hardware { } } -impl HardwareBridge for Hardware { - fn begin_frame(&mut self) { - self.frame_composer.begin_frame(); - } - - fn bind_scene(&mut self, scene_bank_id: usize) -> bool { - self.frame_composer.bind_scene(scene_bank_id) - } - - fn unbind_scene(&mut self) { - self.frame_composer.unbind_scene(); - } - - fn set_camera(&mut self, x: i32, y: i32) { - self.frame_composer.set_camera(x, y); - } - - fn emit_sprite(&mut self, sprite: Sprite) -> bool { - self.frame_composer.emit_sprite(sprite) - } - - fn close_game2d_packet(&self) -> Game2DFramePacket { - self.frame_composer.close_game2d_packet() - } - - fn publish_render_submission(&mut self, submission: &RenderSubmission) { +impl Hardware { + fn consume_render_submission(&mut self, submission: &RenderSubmission) { match &submission.packet { RenderSubmissionPacket::Game2D(packet) => { if packet.composer.bound_scene.is_none() { @@ -94,54 +70,6 @@ impl HardwareBridge for Hardware { } self.gfx.present(); } - - fn render_frame(&mut self) { - if self.frame_composer.active_scene_id().is_none() { - let packet = self.frame_composer.close_game2d_packet(); - self.gfx.render_game2d_frame_packet(&packet); - } else { - self.frame_composer.render_frame(&mut self.gfx); - } - } - - fn has_glyph_bank(&self, bank_id: usize) -> bool { - self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some() - } - - fn gfx(&self) -> &dyn GfxBridge { - &self.gfx - } - fn gfx_mut(&mut self) -> &mut dyn GfxBridge { - &mut self.gfx - } - - fn audio(&self) -> &dyn AudioBridge { - &self.audio - } - fn audio_mut(&mut self) -> &mut dyn AudioBridge { - &mut self.audio - } - - fn pad(&self) -> &dyn PadBridge { - &self.pad - } - fn pad_mut(&mut self) -> &mut dyn PadBridge { - &mut self.pad - } - - fn touch(&self) -> &dyn TouchBridge { - &self.touch - } - fn touch_mut(&mut self) -> &mut dyn TouchBridge { - &mut self.touch - } - - fn assets(&self) -> &dyn AssetBridge { - &self.assets - } - fn assets_mut(&mut self) -> &mut dyn AssetBridge { - &mut self.assets - } } impl RenderSubmissionSink for Hardware { @@ -149,14 +77,19 @@ impl RenderSubmissionSink for Hardware { &mut self, submission: RenderSubmission, ) -> Result<(), RenderSubmitError> { - self.publish_render_submission(&submission); + self.consume_render_submission(&submission); Ok(()) } } impl RenderBackend for Hardware { fn render_frame(&mut self) { - HardwareBridge::render_frame(self); + if self.frame_composer.active_scene_id().is_none() { + let packet = self.frame_composer.close_game2d_packet(); + self.gfx.render_game2d_frame_packet(&packet); + } else { + self.frame_composer.render_frame(&mut self.gfx); + } } } @@ -183,7 +116,7 @@ impl Game2DFrameComposer for Hardware { } fn emit_sprite(&mut self, sprite: Sprite) -> prometeu_hal::ComposerOpStatus { - if !self.has_glyph_bank(sprite.bank_id as usize) { + if self.gfx.glyph_banks.glyph_bank_slot(sprite.bank_id as usize).is_none() { return prometeu_hal::ComposerOpStatus::BankInvalid; } @@ -406,9 +339,12 @@ mod tests { let mut glyph_slots = [None; 16]; glyph_slots[0] = Some(0); hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots); - assert!(HardwareBridge::bind_scene(&mut hardware, 0)); + assert_eq!( + Game2DFrameComposer::bind_scene(&mut hardware, 0), + prometeu_hal::ComposerOpStatus::Ok + ); - let mut packet = HardwareBridge::close_game2d_packet(&hardware); + let mut packet = Game2DFrameComposer::close_game2d_packet(&hardware); packet.gfx2d = vec![Gfx2dCommand::FillRect { rect: Rect { x: 0, y: 0, w: 1, h: 1 }, color: Color::BLUE, @@ -433,7 +369,10 @@ mod tests { let mut glyph_slots = [None; 16]; glyph_slots[0] = Some(0); hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots); - assert!(HardwareBridge::bind_scene(&mut hardware, 0)); + assert_eq!( + Game2DFrameComposer::bind_scene(&mut hardware, 0), + prometeu_hal::ComposerOpStatus::Ok + ); let packet = Game2DFramePacket::new( ComposerFramePacket { @@ -455,7 +394,7 @@ mod tests { Vec::new(), ); - HardwareBridge::begin_frame(&mut hardware); + Game2DFrameComposer::begin_frame(&mut hardware); RenderSubmissionSink::submit_render_submission( &mut hardware, RenderSubmission::game2d(FrameId::ZERO, packet), diff --git a/crates/console/prometeu-hal/src/hardware_bridge.rs b/crates/console/prometeu-hal/src/hardware_bridge.rs deleted file mode 100644 index 6b39c911..00000000 --- a/crates/console/prometeu-hal/src/hardware_bridge.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::asset_bridge::AssetBridge; -use crate::audio_bridge::AudioBridge; -use crate::gfx_bridge::GfxBridge; -use crate::pad_bridge::PadBridge; -use crate::render_submission::{Game2DFramePacket, RenderSubmission}; -use crate::sprite::Sprite; -use crate::touch_bridge::TouchBridge; - -/// Legacy migration scaffold for code that has not moved to `platform` facades yet. -/// -/// `HardwareBridge` is intentionally not the final runtime-facing contract. New -/// platform boundaries belong in `crate::platform`, and this trait must be -/// removed after runtime, firmware, host, and tests finish migrating. -pub trait HardwareBridge { - fn begin_frame(&mut self); - fn bind_scene(&mut self, scene_bank_id: usize) -> bool; - fn unbind_scene(&mut self); - fn set_camera(&mut self, x: i32, y: i32); - fn emit_sprite(&mut self, sprite: Sprite) -> bool; - fn close_game2d_packet(&self) -> Game2DFramePacket; - fn publish_render_submission(&mut self, submission: &RenderSubmission); - fn render_frame(&mut self); - fn has_glyph_bank(&self, bank_id: usize) -> bool; - - fn gfx(&self) -> &dyn GfxBridge; - fn gfx_mut(&mut self) -> &mut dyn GfxBridge; - - fn audio(&self) -> &dyn AudioBridge; - fn audio_mut(&mut self) -> &mut dyn AudioBridge; - - fn pad(&self) -> &dyn PadBridge; - fn pad_mut(&mut self) -> &mut dyn PadBridge; - - fn touch(&self) -> &dyn TouchBridge; - fn touch_mut(&mut self) -> &mut dyn TouchBridge; - - fn assets(&self) -> &dyn AssetBridge; - fn assets_mut(&mut self) -> &mut dyn AssetBridge; -} diff --git a/crates/console/prometeu-hal/src/host_context.rs b/crates/console/prometeu-hal/src/host_context.rs index b4ec4862..463351ed 100644 --- a/crates/console/prometeu-hal/src/host_context.rs +++ b/crates/console/prometeu-hal/src/host_context.rs @@ -1,27 +1,17 @@ -use crate::hardware_bridge::HardwareBridge; use crate::platform::RuntimePlatform; use crate::vm_fault::VmFault; pub struct HostContext<'a> { - pub hw: Option<&'a mut dyn HardwareBridge>, pub platform: Option<&'a mut dyn RuntimePlatform>, } impl<'a> HostContext<'a> { - pub fn new(hw: Option<&'a mut dyn HardwareBridge>) -> Self { - Self { hw, platform: None } + pub fn new(platform: Option<&'a mut dyn RuntimePlatform>) -> Self { + Self { platform } } pub fn new_platform(platform: Option<&'a mut dyn RuntimePlatform>) -> Self { - Self { hw: None, platform } - } - - #[inline] - pub fn require_hw(&mut self) -> Result<&mut dyn HardwareBridge, VmFault> { - match &mut self.hw { - Some(hw) => Ok(*hw), - None => Err(VmFault::Unavailable), - } + Self::new(platform) } #[inline] diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index 1805e122..0c408783 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -11,7 +11,6 @@ pub mod debugger_protocol; pub mod gfx_bridge; pub mod glyph; pub mod glyph_bank; -pub mod hardware_bridge; pub mod host_context; pub mod host_return; pub mod input_signals; @@ -40,7 +39,6 @@ pub use asset_bridge::AssetBridge; pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode}; pub use composer_status::ComposerOpStatus; pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus}; -pub use hardware_bridge::HardwareBridge; pub use host_context::{HostContext, HostContextProvider}; pub use host_return::HostReturn; pub use input_signals::InputSignals; @@ -49,9 +47,8 @@ pub use native_interface::{NativeInterface, SyscallId}; pub use pad_bridge::PadBridge; pub use platform::{ AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform, - LegacyHardwareGame2DFrameComposer, LegacyHardwareRenderSubmissionSink, - LegacyHardwareRuntimePlatform, NoopTelemetryPlatform, RenderBackend, RenderSubmissionSink, - RenderSubmitError, RuntimePlatform, TelemetryPlatform, TestPlatform, + NoopTelemetryPlatform, RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform, + TelemetryPlatform, TestPlatform, }; pub use render_submission::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket, diff --git a/crates/console/prometeu-hal/src/platform.rs b/crates/console/prometeu-hal/src/platform.rs index 94b97292..35855c98 100644 --- a/crates/console/prometeu-hal/src/platform.rs +++ b/crates/console/prometeu-hal/src/platform.rs @@ -1,7 +1,6 @@ use crate::asset_bridge::AssetBridge; use crate::audio_bridge::AudioBridge; use crate::composer_status::ComposerOpStatus; -use crate::hardware_bridge::HardwareBridge; use crate::pad_bridge::PadBridge; use crate::render_submission::{Game2DFramePacket, RenderSubmission}; use crate::sprite::Sprite; @@ -21,26 +20,6 @@ pub trait RenderSubmissionSink { ) -> Result<(), RenderSubmitError>; } -pub struct LegacyHardwareRenderSubmissionSink<'a> { - hw: &'a mut dyn HardwareBridge, -} - -impl<'a> LegacyHardwareRenderSubmissionSink<'a> { - pub fn new(hw: &'a mut dyn HardwareBridge) -> Self { - Self { hw } - } -} - -impl RenderSubmissionSink for LegacyHardwareRenderSubmissionSink<'_> { - fn submit_render_submission( - &mut self, - submission: RenderSubmission, - ) -> Result<(), RenderSubmitError> { - self.hw.publish_render_submission(&submission); - Ok(()) - } -} - pub trait RenderBackend { fn render_frame(&mut self); } @@ -54,138 +33,6 @@ pub trait Game2DFrameComposer { fn close_game2d_packet(&self) -> Game2DFramePacket; } -pub struct LegacyHardwareGame2DFrameComposer<'a> { - hw: &'a mut dyn HardwareBridge, -} - -impl<'a> LegacyHardwareGame2DFrameComposer<'a> { - pub fn new(hw: &'a mut dyn HardwareBridge) -> Self { - Self { hw } - } -} - -impl Game2DFrameComposer for LegacyHardwareGame2DFrameComposer<'_> { - fn begin_frame(&mut self) { - self.hw.begin_frame(); - } - - fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus { - if self.hw.bind_scene(scene_bank_id) { - ComposerOpStatus::Ok - } else { - ComposerOpStatus::SceneUnavailable - } - } - - fn unbind_scene(&mut self) { - self.hw.unbind_scene(); - } - - fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus { - self.hw.set_camera(x, y); - ComposerOpStatus::Ok - } - - fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus { - if !self.hw.has_glyph_bank(sprite.bank_id as usize) { - return ComposerOpStatus::BankInvalid; - } - - if self.hw.emit_sprite(sprite) { - ComposerOpStatus::Ok - } else { - ComposerOpStatus::SpriteOverflow - } - } - - fn close_game2d_packet(&self) -> Game2DFramePacket { - self.hw.close_game2d_packet() - } -} - -pub struct LegacyHardwareRuntimePlatform<'a> { - hw: &'a mut dyn HardwareBridge, -} - -impl<'a> LegacyHardwareRuntimePlatform<'a> { - pub fn new(hw: &'a mut dyn HardwareBridge) -> Self { - Self { hw } - } -} - -impl RenderSubmissionSink for LegacyHardwareRuntimePlatform<'_> { - fn submit_render_submission( - &mut self, - submission: RenderSubmission, - ) -> Result<(), RenderSubmitError> { - self.hw.publish_render_submission(&submission); - Ok(()) - } -} - -impl RenderBackend for LegacyHardwareRuntimePlatform<'_> { - fn render_frame(&mut self) { - self.hw.render_frame(); - } -} - -impl Game2DFrameComposer for LegacyHardwareRuntimePlatform<'_> { - fn begin_frame(&mut self) { - self.hw.begin_frame(); - } - - fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus { - if self.hw.bind_scene(scene_bank_id) { - ComposerOpStatus::Ok - } else { - ComposerOpStatus::SceneUnavailable - } - } - - fn unbind_scene(&mut self) { - self.hw.unbind_scene(); - } - - fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus { - self.hw.set_camera(x, y); - ComposerOpStatus::Ok - } - - fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus { - if !self.hw.has_glyph_bank(sprite.bank_id as usize) { - return ComposerOpStatus::BankInvalid; - } - - if self.hw.emit_sprite(sprite) { - ComposerOpStatus::Ok - } else { - ComposerOpStatus::SpriteOverflow - } - } - - fn close_game2d_packet(&self) -> Game2DFramePacket { - self.hw.close_game2d_packet() - } -} - -impl InputPlatform for LegacyHardwareRuntimePlatform<'_> { - fn pad(&self) -> &dyn PadBridge { - self.hw.pad() - } - - fn pad_mut(&mut self) -> &mut dyn PadBridge { - self.hw.pad_mut() - } - - fn touch(&self) -> &dyn TouchBridge { - self.hw.touch() - } - - fn touch_mut(&mut self) -> &mut dyn TouchBridge { - self.hw.touch_mut() - } -} - pub trait AudioPlatform: AudioBridge {} impl AudioPlatform for T {} @@ -212,8 +59,6 @@ pub struct NoopTelemetryPlatform; impl TelemetryPlatform for NoopTelemetryPlatform {} -static LEGACY_NOOP_TELEMETRY: NoopTelemetryPlatform = NoopTelemetryPlatform; - pub trait RuntimePlatform { fn display_size(&self) -> (usize, usize) { (480, 270) @@ -231,48 +76,6 @@ pub trait RuntimePlatform { fn telemetry(&self) -> &dyn TelemetryPlatform; } -impl RuntimePlatform for LegacyHardwareRuntimePlatform<'_> { - fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink { - self - } - - fn render_backend(&mut self) -> &mut dyn RenderBackend { - self - } - - fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer { - self - } - - fn audio(&self) -> &dyn AudioBridge { - self.hw.audio() - } - - fn audio_mut(&mut self) -> &mut dyn AudioBridge { - self.hw.audio_mut() - } - - fn input(&self) -> &dyn InputPlatform { - self - } - - fn input_mut(&mut self) -> &mut dyn InputPlatform { - self - } - - fn assets(&self) -> &dyn AssetBridge { - self.hw.assets() - } - - fn assets_mut(&mut self) -> &mut dyn AssetBridge { - self.hw.assets_mut() - } - - fn telemetry(&self) -> &dyn TelemetryPlatform { - &LEGACY_NOOP_TELEMETRY - } -} - pub trait TestPlatform: RuntimePlatform {} impl TestPlatform for T {} diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index 4786f5ce..f2d3f065 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -3,7 +3,7 @@ use crate::os::SystemOS; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; -use prometeu_hal::{HardwareBridge, InputSignals, RuntimePlatform}; +use prometeu_hal::{InputSignals, RuntimePlatform}; use prometeu_vm::VirtualMachine; use std::sync::atomic::Ordering; @@ -47,9 +47,9 @@ impl<'a> VmFacade<'a> { pub fn debug_step_instruction( &mut self, vm: &mut VirtualMachine, - hw: &mut dyn HardwareBridge, + platform: &mut dyn RuntimePlatform, ) -> Option { - self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, hw) + self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, platform) } pub fn paused(&self) -> bool { diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index 267852da..ec12b75a 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -9,9 +9,8 @@ use crate::services::memcard::MemcardService; use prometeu_hal::asset::{BankTelemetry, BankType}; use prometeu_hal::log::{LogLevel, LogService, LogSource}; use prometeu_hal::{ - FrameId, HardwareBridge, HostContext, InputSignals, LegacyHardwareRuntimePlatform, - RenderSubmission, RenderSubmissionPacket, RenderSubmissionSink, RuntimePlatform, - ShellUiFramePacket, + FrameId, HostContext, InputSignals, RenderSubmission, RenderSubmissionPacket, + RenderSubmissionSink, RuntimePlatform, ShellUiFramePacket, }; use prometeu_vm::LogicalFrameEndingReason; use std::collections::HashMap; @@ -77,11 +76,10 @@ impl VirtualMachineRuntime { &mut self, log_service: &mut LogService, vm: &mut VirtualMachine, - hw: &mut dyn HardwareBridge, + platform: &mut dyn RuntimePlatform, ) -> Option { let step_result = { - let mut platform = LegacyHardwareRuntimePlatform::new(hw); - let mut ctx = HostContext::new_platform(Some(&mut platform)); + let mut ctx = HostContext::new_platform(Some(platform)); let mut fs = VirtualFS::new(); let mut fs_state = FsState::Unmounted; let mut memcard = MemcardService::new(); diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index 4ebf6291..29ecf6cd 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -1,5 +1,5 @@ -use prometeu_drivers::hardware::Hardware; use prometeu_firmware::{BootTarget, Firmware}; +use prometeu_hal::RuntimePlatform; use prometeu_hal::cartridge_loader::CartridgeLoader; use prometeu_hal::debugger_protocol::*; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; @@ -99,7 +99,7 @@ impl HostDebugger { /// Main maintenance method called by the HostRunner every iteration. /// It handles new connections and processes incoming commands. - pub fn check_commands(&mut self, firmware: &mut Firmware, hardware: &mut Hardware) { + pub fn check_commands(&mut self, firmware: &mut Firmware, platform: &mut dyn RuntimePlatform) { // 1. Accept new client connections. if let Some(listener) = &self.listener && let Ok((stream, _addr)) = listener.accept() @@ -154,7 +154,7 @@ impl HostDebugger { continue; } if let Ok(cmd) = serde_json::from_str::(trimmed) { - self.handle_command(cmd, firmware, hardware); + self.handle_command(cmd, firmware, platform); } } } @@ -182,7 +182,7 @@ impl HostDebugger { &mut self, cmd: DebugCommand, firmware: &mut Firmware, - hardware: &mut Hardware, + platform: &mut dyn RuntimePlatform, ) { match cmd { DebugCommand::Ok | DebugCommand::Start => { @@ -201,7 +201,7 @@ impl HostDebugger { DebugCommand::Step => { // Execute exactly one instruction and keep paused. firmware.os.vm().set_paused(true); - let _ = firmware.os.vm().debug_step_instruction(&mut firmware.vm, hardware); + let _ = firmware.os.vm().debug_step_instruction(&mut firmware.vm, platform); } DebugCommand::StepFrame => { // Execute until the end of the current logical frame. diff --git a/docs/specs/runtime/04-gfx-peripheral.md b/docs/specs/runtime/04-gfx-peripheral.md index 6f545436..1ee1e924 100644 --- a/docs/specs/runtime/04-gfx-peripheral.md +++ b/docs/specs/runtime/04-gfx-peripheral.md @@ -31,6 +31,13 @@ MUST contain a frame id, the active app mode, and one typed packet: immutable. The runtime backpressure policy is latest-complete-submission-wins; it MUST NOT accumulate an unbounded frame queue. +The platform layer is the runtime-facing implementation boundary for render +handoff. Runtime and firmware code publish completed frames through typed +platform services such as `RenderSubmissionSink`, and they access Game 2D +composition through `Game2DFrameComposer`. They MUST NOT depend on a monolithic +hardware bridge, mutable `Hardware`, mutable `Gfx`, or live `FrameComposer` +reference as the render handoff contract. + The current 2D graphics model is based on: - framebuffer @@ -127,6 +134,10 @@ to the logical/runtime side before handoff, or to an owning service. They MUST NOT require the render consumer to hold mutable VM, `Hardware`, `Gfx`, or `FrameComposer` references. +Local host implementations may keep a concrete hardware object internally as a +platform implementation detail. That object is not part of the runtime-facing +render boundary; the boundary is the typed platform service set. + The runtime does not guarantee visual integrity if a developer or framework replaces resources behind an in-flight submission in a way that violates asset discipline. diff --git a/docs/specs/runtime/11-portability-and-cross-platform-execution.md b/docs/specs/runtime/11-portability-and-cross-platform-execution.md index bb6a9d33..845e74d8 100644 --- a/docs/specs/runtime/11-portability-and-cross-platform-execution.md +++ b/docs/specs/runtime/11-portability-and-cross-platform-execution.md @@ -123,9 +123,13 @@ The graphics system: The platform layer: +- exposes typed service facades for render submission, render backend + execution, Game 2D composition, input, audio, assets, and telemetry - consumes closed render submissions through a render-surface implementation - transports published RGBA8888 output into a host presentation surface without injecting host-owned debug overlay pixels +- is the runtime-facing portability boundary; runtime and firmware code must not + depend on a monolithic hardware bridge or on a concrete hardware aggregate The host presentation layer MUST treat RGBA8888 as the canonical logical framebuffer format. RGB565 conversion is not part of the normal host diff --git a/docs/specs/runtime/README.md b/docs/specs/runtime/README.md index 9508d066..a2d3ae67 100644 --- a/docs/specs/runtime/README.md +++ b/docs/specs/runtime/README.md @@ -50,6 +50,12 @@ Does not belong in `docs/runtime/specs/` as the primary canonical source: Here, hardware is not a generic bucket for everything. In the current package, "hardware" means the machine's virtual peripherals: GFX, AUDIO, INPUT, TOUCH, and MEMCARD/save memory. VM, firmware, cartridge, and ABI sit in sibling categories rather than inside hardware. +The implementation boundary for host portability is the platform layer. Runtime, +firmware, and host integration code use typed platform services for rendering, +input, audio, assets, and telemetry. A concrete local hardware aggregate may +exist inside a host or test platform, but it is not the normative runtime-facing +contract. + ## Document Functions - `normative`: defines the technical contract, expected behavior, or implementation-facing surface. From b4f7b4f3c045aea3369c38d22a5b715517e6f8cd Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 11:59:09 +0100 Subject: [PATCH 29/47] clean up debugger platform tests --- .../host/prometeu-host-desktop-winit/src/debugger.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index 29ecf6cd..9bb1ea92 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -398,17 +398,13 @@ mod tests { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); - debugger.handle_command(DebugCommand::Pause, &mut firmware, platform.local_hardware_mut()); + debugger.handle_command(DebugCommand::Pause, &mut firmware, &mut platform); assert!(firmware.os.vm().paused()); - debugger.handle_command(DebugCommand::Resume, &mut firmware, platform.local_hardware_mut()); + debugger.handle_command(DebugCommand::Resume, &mut firmware, &mut platform); assert!(!firmware.os.vm().paused()); - debugger.handle_command( - DebugCommand::StepFrame, - &mut firmware, - platform.local_hardware_mut(), - ); + debugger.handle_command(DebugCommand::StepFrame, &mut firmware, &mut platform); assert!(!firmware.os.vm().paused()); assert!(firmware.os.vm().debug_step_requested()); } @@ -422,7 +418,7 @@ mod tests { debugger.waiting_for_start = true; firmware.os.vm().set_paused(true); - debugger.handle_command(DebugCommand::Start, &mut firmware, platform.local_hardware_mut()); + debugger.handle_command(DebugCommand::Start, &mut firmware, &mut platform); assert!(!debugger.waiting_for_start); assert!(!firmware.os.vm().paused()); From a761e73587fe28c55a8ef44063bf2ef5e953fdc4 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 12:04:43 +0100 Subject: [PATCH 30/47] update real render worker agenda --- discussion/index.ndjson | 2 +- ...D-0043-real-render-worker-establishment.md | 90 ++++++++++++++++--- ...N-0098-define-platform-facade-contracts.md | 2 +- ...-introduce-owned-render-submission-sink.md | 2 +- ...igrate-local-render-publication-to-sink.md | 2 +- ...-remove-immediate-gfx-syscall-rendering.md | 2 +- ...introduce-game2d-frame-composer-service.md | 2 +- ...oduce-runtime-platform-and-testplatform.md | 2 +- ...rate-vm-runtime-hostcontext-to-platform.md | 2 +- ...e-firmware-and-hub-to-platform-services.md | 2 +- ...grate-desktop-host-to-platform-services.md | 2 +- ...0107-migrate-remaining-platform-domains.md | 2 +- .../PLN-0108-migrate-tests-to-testplatform.md | 2 +- ...-remove-hardwarebridge-and-update-specs.md | 2 +- 14 files changed, 93 insertions(+), 23 deletions(-) diff --git a/discussion/index.ndjson b/discussion/index.ndjson index d8d12dae..8e7174a2 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"open","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-06","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06","ref_decisions":["DEC-0032"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md index 18b054da..e9b84cb2 100644 --- a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md +++ b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md @@ -11,9 +11,24 @@ tags: [runtime, renderer, worker, concurrency, host, hal, architecture] ## Contexto -`DEC-0031` definiu o contrato arquitetural da fronteira VM/render. `AGD-0042` prepara a fronteira host/hardware/render para que `Hardware` deixe de ser um objeto concreto atravessando o worker e passe a ser contrato/facades implementadas pelo host. +`DEC-0031` definiu o contrato arquitetural da fronteira VM/render. `AGD-0042` preparou a fronteira host/hardware/render para que `Hardware` deixe de ser um objeto concreto atravessando o worker e passe a ser contrato/facades implementadas pelo host. -Esta agenda discute a implementacao do worker real propriamente dito, assumindo que a preparacao da fronteira foi ou sera resolvida antes da execucao. +Esta agenda discute a implementacao do worker real propriamente dito, assumindo que a preparacao da fronteira ja foi resolvida antes da execucao. + +### Estado Atual Depois da `AGD-0042` + +A preparacao de fronteira foi executada pelos planos `PLN-0098` a `PLN-0109`: + +- `HardwareBridge` foi removido do codigo e deixou de ser contrato publico; +- `RuntimePlatform` passou a ser a fronteira runtime-facing para render, input, audio, assets e telemetry; +- `RenderSubmissionSink` aceita submissao owned; +- `Game2DFrameComposer` separa fechamento logico de frame do backend de render; +- syscalls `gfx2d.*` e `gfxui.*` bufferizam comandos e nao desenham imediatamente via `Gfx`; +- firmware, Hub, runtime e host desktop passam por servicos de plataforma; +- testes de runtime/firmware usam `TestPlatform` como fixture local; +- specs de GFX/portabilidade documentam que o worker/render consumer nao deve depender de `&mut Hardware`, `&mut Gfx`, `FrameComposer` vivo ou estado mutavel da VM. + +O estado atual ainda nao tem worker real. O `LocalRenderWorker` continua sendo o caminho local/cooperativo que consome `RenderSubmission` no mesmo processo. O proximo passo desta agenda e decidir onde vive o controller do worker real, qual handoff thread-safe ele usa, e qual backend renderizavel ele owns ou recebe. ## Problema @@ -124,14 +139,69 @@ O host desktop pode ser a primeira integracao concreta, mas nao deve ser o unico ## Perguntas em Aberto -- [ ] O worker controller vive em `prometeu-system`, `prometeu-hal`, `prometeu-drivers`, ou host? -- [ ] O handoff thread-safe sera `Mutex>`, canal bounded, atomic slot, ou estrutura propria? -- [ ] Como modelar current work para shutdown e stale owner? -- [ ] Quem chama present: worker, host event loop, ou display cadence service? -- [ ] Como o worker recebe recursos read-only preparados na `AGD-0042`? -- [ ] Qual erro tipado minimo precisamos no primeiro worker? -- [ ] Como provar que a VM nao bloqueia quando o worker atrasa? -- [ ] Qual sera o primeiro backend real: desktop `Gfx`/pixels, framebuffer local, ou fake backend? +### 1. Onde vive o worker controller? + +**Pergunta:** O worker controller vive em `prometeu-system`, `prometeu-hal`, `prometeu-drivers`, ou host? + +**Sugestao:** colocar o controller contratual em `prometeu-system`, proximo de `RenderManager`, e manter traits pequenas em `prometeu-hal` quando forem superficie compartilhada. `prometeu-drivers` deve continuar oferecendo implementacoes locais/testaveis; host desktop deve integrar o controller, nao definir o contrato. + +**Motivo:** o `RenderManager` ja owns policy de AppMode, ownership/epoch, latest-wins e telemetry. Se o controller nascer apenas no host, a semantica de worker vira desktop-specific e fica mais dificil provar o contrato sem janela nativa. + +### 2. Qual handoff thread-safe usar? + +**Pergunta:** O handoff thread-safe sera `Mutex>`, canal bounded, atomic slot, ou estrutura propria? + +**Sugestao:** comecar com uma estrutura propria simples baseada em `Mutex> + Condvar`, encapsulada como single-slot latest-wins. O produtor substitui a pending submission sem bloquear em rasterizacao; o consumidor espera por nova submission ou shutdown. + +**Motivo:** canal bounded tende a preservar historico ou bloquear produtor se nao for usado com cuidado. Um slot explicito modela diretamente a regra ja aceita: nao existe fila crescente, a ultima submissao completa vence. + +### 3. Como modelar current work, shutdown e stale owner? + +**Pergunta:** Como modelar current work para shutdown e stale owner? + +**Sugestao:** separar `pending` de `in_flight`. Ao tomar uma submission, o worker carimba `in_flight` localmente, renderiza, e checa ownership/epoch contra um snapshot atomico ou handle compartilhado antes de present. Shutdown sinaliza stop, acorda o worker e faz join bounded; se houver trabalho em andamento, ele pode terminar rasterizacao, mas nao pode apresentar se o owner/epoch estiver obsoleto ou o stop ja exigir descarte. + +**Motivo:** isso evita que shutdown dependa de cancelar mid-raster em codigo que talvez nao seja cancelavel, mas ainda impede present obsoleto. + +### 4. Quem chama present? + +**Pergunta:** Quem chama present: worker, host event loop, ou display cadence service? + +**Sugestao:** no primeiro corte, o worker deve produzir/publish uma superficie pronta e notificar host invalidation; o host event loop continua sendo autoridade de apresentacao da janela nativa. O "repeat ultimo frame valido" deve ser modelado no controller/telemetry como comportamento de consumo/cadence, mas o swap/window present concreto deve respeitar o host. + +**Motivo:** winit/pixels e plataformas graficas frequentemente tem afinidade com thread/event loop. Colocar o swap real dentro do worker pode funcionar em alguns hosts e quebrar portabilidade. O contrato importante e que a VM nao bloqueie e que o host tenha uma superficie publicada coerente. + +### 5. Como o worker recebe recursos read-only? + +**Pergunta:** Como o worker recebe recursos read-only preparados na `AGD-0042`? + +**Sugestao:** o primeiro backend deve receber `RenderSubmission` owned e handles/read-only resource accessors necessarios para resolver glyph/scene banks. Esses accessors devem ser compartilhaveis e imutaveis durante o consumo, evitando `&mut Hardware`, `&mut Gfx` e `FrameComposer` vivo. + +**Motivo:** `AGD-0042` eliminou o bridge monolitico exatamente para impedir que o worker dependa do hardware inteiro. A proxima decisao precisa declarar quais recursos sao lidos pelo render backend e como sua estabilidade e garantida durante uma submissao em voo. + +### 6. Qual erro tipado minimo? + +**Pergunta:** Qual erro tipado minimo precisamos no primeiro worker? + +**Sugestao:** introduzir um erro pequeno, por exemplo `RenderWorkerError`, cobrindo pelo menos: backend unavailable, render failed, present/publish failed, shutdown timeout, stale submission discarded. Panics continuam sendo capturados como falha interna, mas nao devem ser o modelo operacional normal. + +**Motivo:** `LocalRenderWorker` hoje prova policy, mas nao diferencia falhas reais de render/present. O worker real precisa reportar falhas sem contaminar a VM com detalhes de host. + +### 7. Como provar que a VM nao bloqueia? + +**Pergunta:** Como provar que a VM nao bloqueia quando o worker atrasa? + +**Sugestao:** criar testes deterministas com backend fake controlado por barreiras/condvars, nao sleeps. O teste deve segurar o worker em rasterizacao, produzir frames adicionais no runtime, verificar substituicao latest-wins/drop telemetry, e provar que o tick da VM retorna sem esperar o consumo terminar. + +**Motivo:** sleeps tornam teste de concorrencia fragil. Barreiras permitem provar especificamente a propriedade desejada: produtor nao bloqueia em raster/present lento. + +### 8. Qual primeiro backend real? + +**Pergunta:** Qual sera o primeiro backend real: desktop `Gfx`/pixels, framebuffer local, ou fake backend? + +**Sugestao:** implementar primeiro um backend fake/mocked para contrato e um backend local framebuffer para integracao sem janela. A integracao desktop deve vir em seguida como consumidor concreto, usando a mesma trait. + +**Motivo:** se o primeiro backend for desktop/winit, o contrato corre risco de nascer acoplado a detalhes de janela. Se o primeiro for fake/local, a semantica do worker fica testavel antes da integracao visual. ## Criterio para Encerrar diff --git a/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md b/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md index 2ec7f397..f29b2154 100644 --- a/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md +++ b/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md @@ -2,7 +2,7 @@ id: PLN-0098 ticket: real-render-worker-establishment title: Define Platform Facade Contracts -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md b/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md index 844568be..17651383 100644 --- a/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md +++ b/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md @@ -2,7 +2,7 @@ id: PLN-0099 ticket: real-render-worker-establishment title: Introduce Owned RenderSubmissionSink -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md b/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md index 670963e4..550b66a6 100644 --- a/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md +++ b/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md @@ -2,7 +2,7 @@ id: PLN-0100 ticket: real-render-worker-establishment title: Migrate Local Render Publication to RenderSubmissionSink -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md b/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md index 69975865..bb4ffa76 100644 --- a/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md +++ b/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md @@ -2,7 +2,7 @@ id: PLN-0101 ticket: real-render-worker-establishment title: Remove Immediate Gfx2D and GfxUI Syscall Rendering -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md b/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md index 0f715ca9..afad26c2 100644 --- a/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md +++ b/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md @@ -2,7 +2,7 @@ id: PLN-0102 ticket: real-render-worker-establishment title: Introduce Game2DFrameComposer Logical Service -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md b/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md index ee18af08..86224a60 100644 --- a/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md +++ b/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md @@ -2,7 +2,7 @@ id: PLN-0103 ticket: real-render-worker-establishment title: Introduce RuntimePlatform and TestPlatform -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md b/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md index 1b00bc5a..1cdc4475 100644 --- a/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md +++ b/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md @@ -2,7 +2,7 @@ id: PLN-0104 ticket: real-render-worker-establishment title: Migrate VM Runtime HostContext to Platform Services -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md b/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md index a7165b5e..1a574ea0 100644 --- a/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md +++ b/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md @@ -2,7 +2,7 @@ id: PLN-0105 ticket: real-render-worker-establishment title: Migrate Firmware and Hub to Platform Services -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md b/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md index 016ccd71..499a22a9 100644 --- a/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md +++ b/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md @@ -2,7 +2,7 @@ id: PLN-0106 ticket: real-render-worker-establishment title: Migrate Desktop Host to Platform Services -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md b/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md index e7f13106..a19982cf 100644 --- a/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md +++ b/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md @@ -2,7 +2,7 @@ id: PLN-0107 ticket: real-render-worker-establishment title: Migrate Remaining Platform Domains -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md b/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md index cd08c9eb..ce6871a0 100644 --- a/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md +++ b/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md @@ -2,7 +2,7 @@ id: PLN-0108 ticket: real-render-worker-establishment title: Migrate Tests to TestPlatform -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] diff --git a/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md b/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md index f795d8c0..a44508da 100644 --- a/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md +++ b/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md @@ -2,7 +2,7 @@ id: PLN-0109 ticket: real-render-worker-establishment title: Remove HardwareBridge and Update Specs -status: open +status: done created: 2026-06-06 completed: ref_decisions: [DEC-0032] From 373f842bef0575054e1ce298ff9ef8c4420e35e9 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 12:15:57 +0100 Subject: [PATCH 31/47] align render worker agenda direction --- ...GD-0043-real-render-worker-establishment.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md index e9b84cb2..af4d6edd 100644 --- a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md +++ b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md @@ -167,9 +167,9 @@ O host desktop pode ser a primeira integracao concreta, mas nao deve ser o unico **Pergunta:** Quem chama present: worker, host event loop, ou display cadence service? -**Sugestao:** no primeiro corte, o worker deve produzir/publish uma superficie pronta e notificar host invalidation; o host event loop continua sendo autoridade de apresentacao da janela nativa. O "repeat ultimo frame valido" deve ser modelado no controller/telemetry como comportamento de consumo/cadence, mas o swap/window present concreto deve respeitar o host. +**Sugestao:** no primeiro corte, o worker deve produzir/publish uma superficie pronta como buffer RGBA8888 owned, por exemplo `OwnedRgba8888Frame`. O host event loop continua sendo autoridade de apresentacao da janela nativa. O "repeat ultimo frame valido" deve repetir o ultimo buffer publicado, nao recompor nem exigir que o worker conheca a API nativa de janela/swap. -**Motivo:** winit/pixels e plataformas graficas frequentemente tem afinidade com thread/event loop. Colocar o swap real dentro do worker pode funcionar em alguns hosts e quebrar portabilidade. O contrato importante e que a VM nao bloqueie e que o host tenha uma superficie publicada coerente. +**Motivo:** RGBA8888 ja e o formato logico canonico nas specs. Um frame owned com `FrameId`, ownership/epoch, largura, altura, stride e pixels RGBA8888 deixa o contrato testavel sem janela e evita acoplamento a winit/pixels, SDL, swapchain ou textura nativa. Colocar o swap real dentro do worker pode funcionar em alguns hosts e quebrar portabilidade. O contrato importante e que a VM nao bloqueie e que o host tenha uma superficie publicada coerente. ### 5. Como o worker recebe recursos read-only? @@ -219,5 +219,19 @@ Esta agenda pode virar decision quando tivermos: ## Discussion - 2026-06-06: Agenda criada dentro da `DSC-0042` para separar worker real da preparacao de fronteira discutida em `AGD-0042`. +- 2026-06-15: Apos a conclusao dos planos `PLN-0098` a `PLN-0109`, a direcao da agenda foi alinhada em torno da Opcao C: controller em `prometeu-system`, handoff single-slot thread-safe, backend fake/local primeiro, e publicacao de frame owned RGBA8888 antes da integracao desktop concreta. ## Resolution + +Direcao proposta para decisao: + +- `RenderManager` continua como autoridade de policy, ownership/epoch, latest-wins e telemetry; +- o worker controller contratual deve viver em `prometeu-system`; +- traits compartilhadas minimas podem viver em `prometeu-hal`; +- o handoff real deve ser single-slot latest-wins com sincronizacao explicita, inicialmente `Mutex> + Condvar`; +- o worker separa `pending` de `in_flight` e checa stop/ownership/epoch antes de publicar; +- o worker publica um `OwnedRgba8888Frame` ou equivalente, contendo frame id, owner/epoch, dimensoes, stride e pixels RGBA8888 owned; +- o host event loop faz o upload/present nativo a partir do ultimo frame publicado; +- o primeiro backend deve ser fake/local para provar contrato e concorrencia sem janela; +- a integracao desktop concreta vem depois, usando a mesma trait/backend contract; +- testes devem usar barreiras/condvars para provar ausencia de bloqueio da VM, latest-wins, stale discard, shutdown bounded e telemetry. From 1022750c12c689f0e037624d7e0c26677cd4826f Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 12:28:25 +0100 Subject: [PATCH 32/47] resolve render worker agenda questions --- ...D-0043-real-render-worker-establishment.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md index af4d6edd..122fc8eb 100644 --- a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md +++ b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md @@ -159,7 +159,7 @@ O host desktop pode ser a primeira integracao concreta, mas nao deve ser o unico **Pergunta:** Como modelar current work para shutdown e stale owner? -**Sugestao:** separar `pending` de `in_flight`. Ao tomar uma submission, o worker carimba `in_flight` localmente, renderiza, e checa ownership/epoch contra um snapshot atomico ou handle compartilhado antes de present. Shutdown sinaliza stop, acorda o worker e faz join bounded; se houver trabalho em andamento, ele pode terminar rasterizacao, mas nao pode apresentar se o owner/epoch estiver obsoleto ou o stop ja exigir descarte. +**Sugestao:** separar `pending` de `in_flight`. Ao tomar uma submission, o worker carimba `in_flight` localmente, renderiza, e checa ownership/epoch contra um snapshot atomico ou handle compartilhado antes de present. Shutdown sinaliza stop, acorda o worker e faz join bounded; se houver trabalho em andamento, ele pode terminar rasterizacao, mas nao pode apresentar se o owner/epoch estiver obsoleto ou o stop ja exigir descarte. O timeout deve ser configuravel; como default inicial, usar `250ms`, que e longo o bastante para evitar falso positivo em desktop comum e curto o bastante para detectar worker preso sem travar encerramento. **Motivo:** isso evita que shutdown dependa de cancelar mid-raster em codigo que talvez nao seja cancelavel, mas ainda impede present obsoleto. @@ -167,17 +167,17 @@ O host desktop pode ser a primeira integracao concreta, mas nao deve ser o unico **Pergunta:** Quem chama present: worker, host event loop, ou display cadence service? -**Sugestao:** no primeiro corte, o worker deve produzir/publish uma superficie pronta como buffer RGBA8888 owned, por exemplo `OwnedRgba8888Frame`. O host event loop continua sendo autoridade de apresentacao da janela nativa. O "repeat ultimo frame valido" deve repetir o ultimo buffer publicado, nao recompor nem exigir que o worker conheca a API nativa de janela/swap. +**Sugestao:** no primeiro corte, o worker deve produzir/publish uma superficie pronta como buffer RGBA8888 owned, por exemplo `OwnedRgba8888Frame`, definido em `prometeu-hal`. O host event loop continua sendo autoridade de apresentacao da janela nativa. O "repeat ultimo frame valido" deve repetir o ultimo buffer publicado, nao recompor nem exigir que o worker conheca a API nativa de janela/swap. -**Motivo:** RGBA8888 ja e o formato logico canonico nas specs. Um frame owned com `FrameId`, ownership/epoch, largura, altura, stride e pixels RGBA8888 deixa o contrato testavel sem janela e evita acoplamento a winit/pixels, SDL, swapchain ou textura nativa. Colocar o swap real dentro do worker pode funcionar em alguns hosts e quebrar portabilidade. O contrato importante e que a VM nao bloqueie e que o host tenha uma superficie publicada coerente. +**Motivo:** RGBA8888 ja e o formato logico canonico nas specs. Um frame owned com `FrameId`, ownership/epoch, largura, altura, `stride_pixels` e pixels RGBA8888 em `Vec` deixa o contrato testavel sem janela e evita acoplamento a winit/pixels, SDL, swapchain ou textura nativa. Colocar o swap real dentro do worker pode funcionar em alguns hosts e quebrar portabilidade. O contrato importante e que a VM nao bloqueie e que o host tenha uma superficie publicada coerente. ### 5. Como o worker recebe recursos read-only? **Pergunta:** Como o worker recebe recursos read-only preparados na `AGD-0042`? -**Sugestao:** o primeiro backend deve receber `RenderSubmission` owned e handles/read-only resource accessors necessarios para resolver glyph/scene banks. Esses accessors devem ser compartilhaveis e imutaveis durante o consumo, evitando `&mut Hardware`, `&mut Gfx` e `FrameComposer` vivo. +**Sugestao:** o primeiro backend deve receber `RenderSubmission` owned e uma interface compacta de acesso read-only aos banks por id. Banks sao read-only por definicao durante o consumo de render; o worker resolve recursos por ids estaveis, sem copiar bank, sem snapshot de bank e sem depender de `Arc` como requisito do contrato. -**Motivo:** `AGD-0042` eliminou o bridge monolitico exatamente para impedir que o worker dependa do hardware inteiro. A proxima decisao precisa declarar quais recursos sao lidos pelo render backend e como sua estabilidade e garantida durante uma submissao em voo. +**Motivo:** `AGD-0042` eliminou o bridge monolitico exatamente para impedir que o worker dependa do hardware inteiro. A proxima decisao precisa declarar quais recursos sao lidos pelo render backend e como sua estabilidade e garantida durante uma submissao em voo. A estabilidade vem da regra de leitura: o worker ve banks por id via interface read-only, enquanto instalacao/mutacao de bank permanece fora do consumo renderizavel em voo. ### 6. Qual erro tipado minimo? @@ -199,9 +199,9 @@ O host desktop pode ser a primeira integracao concreta, mas nao deve ser o unico **Pergunta:** Qual sera o primeiro backend real: desktop `Gfx`/pixels, framebuffer local, ou fake backend? -**Sugestao:** implementar primeiro um backend fake/mocked para contrato e um backend local framebuffer para integracao sem janela. A integracao desktop deve vir em seguida como consumidor concreto, usando a mesma trait. +**Sugestao:** implementar primeiro um backend fake/mocked para contrato e um backend local framebuffer para integracao sem janela. A integracao desktop vem em seguida como consumidor concreto, usando a mesma trait. Ao final desta agenda, o worker deve estar funcionando completamente no caminho host real; a divisao em planos serve para controlar risco, nao para reduzir o alvo final. -**Motivo:** se o primeiro backend for desktop/winit, o contrato corre risco de nascer acoplado a detalhes de janela. Se o primeiro for fake/local, a semantica do worker fica testavel antes da integracao visual. +**Motivo:** se o primeiro backend for desktop/winit, o contrato corre risco de nascer acoplado a detalhes de janela. Se o primeiro for fake/local, a semantica do worker fica testavel antes da integracao visual. Ainda assim, a agenda so deve ser considerada concluida quando a integracao host real tambem estiver operando sobre o worker. ## Criterio para Encerrar @@ -220,6 +220,7 @@ Esta agenda pode virar decision quando tivermos: - 2026-06-06: Agenda criada dentro da `DSC-0042` para separar worker real da preparacao de fronteira discutida em `AGD-0042`. - 2026-06-15: Apos a conclusao dos planos `PLN-0098` a `PLN-0109`, a direcao da agenda foi alinhada em torno da Opcao C: controller em `prometeu-system`, handoff single-slot thread-safe, backend fake/local primeiro, e publicacao de frame owned RGBA8888 antes da integracao desktop concreta. +- 2026-06-15: Perguntas restantes alinhadas: `OwnedRgba8888Frame` deve viver em `prometeu-hal` com pixels `Vec` RGBA8888; banks sao acessados por interface compacta read-only via ids, sem copia/snapshot/`Arc` como contrato; shutdown deve ter timeout configuravel com default inicial sugerido de `250ms`; a agenda pode virar varios plans, mas so fecha quando o worker funcionar no host real. ## Resolution @@ -230,8 +231,11 @@ Direcao proposta para decisao: - traits compartilhadas minimas podem viver em `prometeu-hal`; - o handoff real deve ser single-slot latest-wins com sincronizacao explicita, inicialmente `Mutex> + Condvar`; - o worker separa `pending` de `in_flight` e checa stop/ownership/epoch antes de publicar; -- o worker publica um `OwnedRgba8888Frame` ou equivalente, contendo frame id, owner/epoch, dimensoes, stride e pixels RGBA8888 owned; +- shutdown deve ser bounded e configuravel; default inicial sugerido: `250ms`; +- o worker publica um `OwnedRgba8888Frame` em `prometeu-hal`, contendo frame id, owner/epoch, dimensoes, `stride_pixels` e pixels RGBA8888 owned em `Vec`; +- o worker acessa banks por ids estaveis atraves de uma interface compacta read-only; nao copia banks, nao depende de snapshots de bank e nao usa `Arc` como parte obrigatoria do contrato; - o host event loop faz o upload/present nativo a partir do ultimo frame publicado; - o primeiro backend deve ser fake/local para provar contrato e concorrencia sem janela; - a integracao desktop concreta vem depois, usando a mesma trait/backend contract; +- a agenda pode ser executada em varios plans, mas o criterio final e ter o worker funcionando completamente no host real; - testes devem usar barreiras/condvars para provar ausencia de bloqueio da VM, latest-wins, stale discard, shutdown bounded e telemetry. From cafbc420189ebf89806ad2955ad4fbeaa2485acc Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 12:31:01 +0100 Subject: [PATCH 33/47] accept real render worker decision --- discussion/index.ndjson | 4 +- ...D-0043-real-render-worker-establishment.md | 2 +- .../DEC-0033-real-render-worker-contract.md | 296 ++++++++++++++++++ 3 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 8e7174a2..9794dfd3 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":33,"PLN":110,"LSN":49,"CLSN":1}} +{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":34,"PLN":110,"LSN":49,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"open","created_at":"2026-06-06","updated_at":"2026-06-06"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md index 122fc8eb..dd671366 100644 --- a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md +++ b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md @@ -2,7 +2,7 @@ id: AGD-0043 ticket: real-render-worker-establishment title: Real Render Worker Establishment -status: open +status: accepted created: 2026-06-06 resolved: decision: diff --git a/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md b/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md new file mode 100644 index 00000000..59cbe397 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md @@ -0,0 +1,296 @@ +--- +id: DEC-0033 +ticket: real-render-worker-establishment +title: Real Render Worker Contract +status: accepted +created: 2026-06-15 +accepted: +agenda: AGD-0043 +plans: [] +tags: [runtime, renderer, worker, concurrency, host, hal, architecture] +--- + +## Status + +Decision accepted from agenda `AGD-0043`. + +This is the normative contract for implementation plans that establish the real render worker for Prometeu. + +## Contexto + +`DEC-0031` established the VM/render boundary around closed render submissions, single-slot handoff, frame pacing, AppMode policy, ownership epoch, and render telemetry. + +`DEC-0032` and plans `PLN-0098` through `PLN-0109` prepared the platform boundary required for a real worker: + +- `HardwareBridge` was removed from production/runtime contracts; +- `RuntimePlatform` became the runtime-facing platform service boundary; +- `RenderSubmissionSink` accepts owned render submissions; +- `Game2DFrameComposer` separates logical Game 2D frame closure from backend rendering; +- `gfx2d.*` and `gfxui.*` syscalls buffer commands instead of mutating `Gfx` immediately; +- firmware, Hub, runtime, host desktop, and tests moved to platform services; +- specs now state that worker/render consumers must not depend on `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state. + +The remaining work is to replace the cooperative/local `LocalRenderWorker` proof with a real worker that drains render submissions without blocking VM progress and publishes complete RGBA8888 frames for host presentation. + +## Decisao + +Prometeu SHALL implement the real render worker as a runtime-owned worker controller with host-provided backend integration. + +The worker controller SHALL live in `prometeu-system`, near `RenderManager`, because `RenderManager` owns AppMode policy, ownership/epoch, latest-wins handoff semantics, and render telemetry. + +Shared contract types and traits SHALL live in `prometeu-hal` when they cross runtime/host/driver boundaries. Local/default and test implementations SHALL live in `prometeu-drivers` or host crates as appropriate. + +The worker SHALL consume owned `RenderSubmission` values through a thread-safe single-slot latest-wins handoff. The VM producer MUST NOT block on rasterization, publication, presentation, or worker completion. + +The worker SHALL publish owned RGBA8888 frames, not native window/swapchain objects. The initial shared frame type SHALL be `OwnedRgba8888Frame` in `prometeu-hal`. + +The host event loop SHALL remain responsible for native window upload/present. The worker MUST NOT depend on winit, pixels, SDL, swapchain handles, native textures, or host window APIs. + +The agenda may be implemented through multiple plans, but `AGD-0043` is complete only when the worker functions on the real host path, not merely in fake/local tests. + +## Rationale + +The real worker must prove the architecture promised by `DEC-0031`: VM execution and render consumption can proceed independently without crossing mutable runtime state into the renderer. + +Putting the controller in `prometeu-system` keeps render policy in the same layer that already owns frame lifecycle, ownership epochs, latest-wins behavior, and telemetry. Putting it only in the host would make the contract desktop-specific and harder to test without a window. + +Publishing an owned RGBA8888 frame keeps the worker host-agnostic. The host can upload that frame to any native presentation backend while the worker stays testable through ordinary memory comparisons. + +Using a single explicit pending slot directly models the accepted backpressure rule. Channels or queues can accidentally preserve history or block producers. The runtime contract is not "render every produced frame"; it is "the latest complete submission wins and the VM does not wait for the consumer." + +Read-only bank access by id preserves the no-copy resource boundary. Banks are render resources, not payloads to duplicate into each submission. The worker should resolve resource ids through a compact read-only interface without taking `&mut Hardware`, `&mut Gfx`, a live `FrameComposer`, or VM state. + +## Invariantes / Contrato + +### 1. Runtime controller ownership + +The worker controller MUST live in `prometeu-system`. + +`RenderManager` SHALL remain the authority for: + +- AppMode render policy; +- ownership/epoch; +- latest-wins handoff semantics; +- render telemetry; +- stale submission discard; +- shutdown/request-stop coordination visible to runtime policy. + +Host crates MAY instantiate or wire the controller, but they MUST NOT redefine worker semantics. + +### 2. Shared HAL contracts + +`prometeu-hal` SHALL define shared types/traits that cross the runtime/host/driver boundary. + +At minimum, the worker work SHALL introduce: + +- `OwnedRgba8888Frame`; +- compact read-only render resource access contracts for bank lookup by id; +- typed worker/backend error contracts where they are shared across crates. + +### 3. Owned RGBA8888 frame publication + +`OwnedRgba8888Frame` SHALL represent a completed logical frame ready for host upload/present. + +It MUST include: + +- `FrameId`; +- render ownership/epoch metadata; +- width; +- height; +- `stride_pixels`; +- owned RGBA8888 pixel data as `Vec`. + +The pixel data MUST use the canonical RGBA8888 raw value contract already used by the runtime specs. It MUST NOT be described as native-endian host pixels, native texture data, or backend-specific surface memory. + +Repeating the last valid frame SHALL mean reusing the last published `OwnedRgba8888Frame`, not recomposing from VM state or live composer state. + +### 4. Thread-safe latest-wins handoff + +The real handoff SHALL be a thread-safe single-slot latest-wins structure. + +The initial implementation SHOULD use an explicit slot based on `Mutex> + Condvar`. + +Producer behavior: + +- publish replaces the pending submission if one already exists; +- replacement increments drop/replacement telemetry; +- publish MUST NOT wait for rasterization or present; +- publish MUST wake the consumer when needed. + +Consumer behavior: + +- wait for pending submission or shutdown; +- take ownership of the latest pending submission; +- leave the pending slot empty while work is in flight; +- discard stale work before publication if ownership/epoch no longer matches. + +### 5. Pending and in-flight are separate + +The worker MUST distinguish pending submissions from in-flight work. + +Once the worker takes a submission, that submission is in flight. New producer submissions may replace the pending slot while the worker is still rasterizing the previous in-flight submission. + +Before publishing an in-flight frame, the worker MUST check: + +- stop/shutdown state; +- render owner; +- render epoch/generation. + +If the frame is stale, the worker MUST discard it and update telemetry. It MUST NOT publish stale pixels. + +### 6. Shutdown is bounded + +Worker shutdown MUST be bounded and observable. + +The shutdown timeout MUST be configurable. The initial default SHOULD be `250ms`. + +On shutdown: + +- stop state MUST be signaled; +- the worker MUST be woken if it is waiting; +- join MUST complete within the configured bound or return/report a `ShutdownTimeout` failure; +- in-flight work MAY finish rasterization, but MUST NOT publish after stop/epoch invalidation says it is stale. + +### 7. Read-only bank access by id + +The render backend SHALL resolve render resources through a compact read-only interface by stable ids. + +Banks are read-only by definition during render consumption. The worker MUST NOT copy entire banks into submissions. The worker MUST NOT require bank snapshots. The worker MUST NOT require `Arc` as part of the public contract. + +Implementation may use any internal storage strategy that satisfies the public contract, but the render worker contract is ids plus read-only access. + +Resource installation, mutation, and residency changes remain outside in-flight render consumption. + +### 8. Backend and host presentation split + +The worker backend SHALL consume `RenderSubmission` plus read-only resource access and produce `OwnedRgba8888Frame`. + +The host event loop SHALL upload/present the latest published frame through its native API. + +The worker MUST NOT call native window present directly unless a future decision revises the host presentation model. + +### 9. Typed error model + +The worker work SHALL introduce a minimal typed error model, such as `RenderWorkerError`. + +The error model MUST cover at least: + +- backend unavailable; +- render failed; +- publish/present handoff failed; +- shutdown timeout; +- stale submission discarded; +- worker panic captured as internal failure. + +Panic containment MAY remain as a safety net. Ordinary worker/backend failure MUST NOT rely on panic as the normal reporting path. + +### 10. Telemetry + +Worker telemetry SHALL preserve and extend the existing render telemetry model. + +At minimum, telemetry SHOULD include: + +- submissions produced; +- pending submissions replaced/dropped; +- submissions consumed; +- frames published/presented; +- stale submissions discarded; +- render failures; +- repeated-frame count; +- shutdown timeout count. + +Telemetry MUST NOT change VM semantics. + +### 11. Test contract + +The worker contract MUST be proven without a native window. + +Tests SHALL use deterministic synchronization primitives such as barriers and condvars, not timing sleeps as the primary proof mechanism. + +The test matrix MUST prove: + +- VM producer does not block when worker rasterization is held; +- latest-wins replacement occurs while a frame is in flight; +- stale owner/epoch prevents publication; +- shutdown is bounded; +- typed errors and telemetry are emitted; +- repeat uses the last published `OwnedRgba8888Frame`; +- fake/local backend behavior matches the contract before desktop integration. + +### 12. Final scope + +The implementation MAY be split into multiple plans. + +However, the agenda is not complete until the real host path uses the worker. A fake/local backend is required for contract tests, but it is not sufficient as the final state of `AGD-0043`. + +## Impactos + +### Spec + +Runtime specs must document: + +- `OwnedRgba8888Frame`; +- the thread-safe single-slot latest-wins worker handoff; +- the worker/host presentation split; +- read-only bank access by id; +- shutdown and typed error behavior; +- telemetry expectations. + +### Runtime + +`prometeu-system` must add the worker controller and integrate it with `RenderManager`. + +The VM tick path must submit render work without blocking on worker consumption. + +### HAL + +`prometeu-hal` must expose shared worker contracts: + +- owned RGBA8888 frame type; +- render resource read-only access traits; +- worker/backend error types as needed. + +### Drivers + +`prometeu-drivers` must provide fake/local implementations suitable for deterministic tests and non-window integration. + +Concrete local rendering may continue to use existing GFX machinery internally, but must expose the worker-facing contract without leaking `&mut Hardware`, `&mut Gfx`, or live `FrameComposer`. + +### Host + +The desktop host must integrate the real worker path and upload/present the latest published `OwnedRgba8888Frame` through its native window backend. + +The host owns native presentation; the worker owns render consumption and frame publication. + +### Tests + +Tests must cover concurrency behavior deterministically and must include final host-path integration evidence. + +## Referencias + +- `AGD-0043`: Real Render Worker Establishment. +- `AGD-0042`: Host Hardware and Render Boundary Preparation. +- `DEC-0031`: VM and Render Parallel Execution Boundary. +- `DEC-0032`: Platform Layer and HardwareBridge Elimination. +- `LSN-0048`: Render Workers Need a Closed Packet Contract. +- `docs/specs/runtime/04-gfx-peripheral.md`. +- `docs/specs/runtime/11-portability-and-cross-platform-execution.md`. + +## Propagacao Necessaria + +Create implementation plans before editing specs or code. + +Recommended plan sequence: + +1. define `OwnedRgba8888Frame`, read-only bank access contracts, and worker error types in HAL; +2. introduce the thread-safe latest-wins handoff and deterministic fake/local worker tests; +3. implement worker controller lifecycle, stop token, bounded shutdown, pending/in-flight separation, and telemetry in `prometeu-system`; +4. implement fake/local backend that consumes `RenderSubmission` and produces `OwnedRgba8888Frame`; +5. integrate runtime render submission path with the worker without blocking VM ticks; +6. add stale owner/epoch discard and repeat-last-published-frame behavior; +7. integrate desktop host upload/present from the latest published `OwnedRgba8888Frame`; +8. update specs and final validation tests. + +## Revision Log + +- 2026-06-15: Initial decision draft generated from accepted `AGD-0043`. From 3916a652b23f84e993d1841b7c0a0a84bdfc42ee Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 15:49:17 +0100 Subject: [PATCH 34/47] add real render worker implementation plans --- discussion/index.ndjson | 4 +- .../DEC-0033-real-render-worker-contract.md | 2 +- ...10-define-owned-rgba8888-frame-contract.md | 79 ++++++++++++++++ ...define-read-only-render-resource-access.md | 79 ++++++++++++++++ ...fine-render-worker-errors-and-telemetry.md | 78 ++++++++++++++++ ...plement-thread-safe-latest-wins-handoff.md | 80 ++++++++++++++++ ...eterministic-render-worker-test-harness.md | 78 ++++++++++++++++ ...ment-render-worker-controller-lifecycle.md | 87 +++++++++++++++++ ...116-implement-fake-local-render-backend.md | 78 ++++++++++++++++ ...ntegrate-runtime-submission-with-worker.md | 80 ++++++++++++++++ ...d-stale-epoch-and-repeat-frame-behavior.md | 85 +++++++++++++++++ ...egrate-desktop-host-worker-presentation.md | 81 ++++++++++++++++ ...LN-0120-update-real-render-worker-specs.md | 79 ++++++++++++++++ ...al-worker-path-validation-and-hardening.md | 93 +++++++++++++++++++ 14 files changed, 980 insertions(+), 3 deletions(-) create mode 100644 discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md create mode 100644 discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md create mode 100644 discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md create mode 100644 discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md create mode 100644 discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md create mode 100644 discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md create mode 100644 discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md create mode 100644 discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md create mode 100644 discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md create mode 100644 discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md create mode 100644 discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md create mode 100644 discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 9794dfd3..aef7f6c9 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":34,"PLN":110,"LSN":49,"CLSN":1}} +{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":34,"PLN":122,"LSN":49,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md b/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md index 59cbe397..9d24b9f4 100644 --- a/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md +++ b/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md @@ -6,7 +6,7 @@ status: accepted created: 2026-06-15 accepted: agenda: AGD-0043 -plans: [] +plans: [PLN-0110, PLN-0111, PLN-0112, PLN-0113, PLN-0114, PLN-0115, PLN-0116, PLN-0117, PLN-0118, PLN-0119, PLN-0120, PLN-0121] tags: [runtime, renderer, worker, concurrency, host, hal, architecture] --- diff --git a/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md b/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md new file mode 100644 index 00000000..8cffebcb --- /dev/null +++ b/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md @@ -0,0 +1,79 @@ +--- +id: PLN-0110 +ticket: real-render-worker-establishment +title: Define Owned RGBA8888 Frame Contract +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, hal, frame-contract] +--- + +## Briefing + +`DEC-0033` requires the worker to publish an owned RGBA8888 frame that is independent of native window APIs. This plan defines that shared HAL contract before any worker implementation depends on it. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Introduce `OwnedRgba8888Frame` in `prometeu-hal` as the canonical worker-to-host published frame type. + +## Escopo + +- Add the shared frame type to HAL. +- Include frame id, ownership/epoch metadata, dimensions, stride, and owned `Vec` RGBA8888 pixels. +- Add constructors and validation helpers that keep dimensions, stride, and pixel length coherent. +- Add unit tests for layout and validation. + +## Fora de Escopo + +- No worker thread. +- No host upload/present integration. +- No resource bank access. +- No spec updates beyond code-facing rustdoc if useful. + +## Plano de Execucao + +### Step 1 - Add HAL frame module + +**What:** Add `OwnedRgba8888Frame`. +**How:** Create a HAL module, likely `crates/console/prometeu-hal/src/owned_frame.rs` or equivalent, and export it from `lib.rs`. +**File(s):** `crates/console/prometeu-hal/src/lib.rs`, new HAL frame module. + +### Step 2 - Define required fields + +**What:** Encode the DEC-0033 frame contract. +**How:** Include `FrameId`, render ownership/epoch metadata compatible with `RenderSubmission`, `width`, `height`, `stride_pixels`, and `pixels: Vec`. +**File(s):** HAL frame module, existing render ownership types if reuse is needed. + +### Step 3 - Add constructors and validation + +**What:** Prevent malformed frame buffers from entering the worker-host boundary. +**How:** Add checked constructor(s) that reject zero dimensions, too-small stride, and pixel buffers shorter than `stride_pixels * height`. +**File(s):** HAL frame module. + +### Step 4 - Add tests + +**What:** Pin layout semantics. +**How:** Test valid construction, invalid stride, invalid pixel length, and that raw pixel values are preserved as RGBA8888 `u32`. +**File(s):** HAL frame module tests. + +## Criterios de Aceite + +- [ ] `OwnedRgba8888Frame` is exported by `prometeu-hal`. +- [ ] The type contains `FrameId`, ownership/epoch, width, height, `stride_pixels`, and `Vec` pixels. +- [ ] Invalid dimensions/stride/pixel length cannot be silently accepted by checked constructors. +- [ ] No native host/window/pixels/winit/SDL type appears in the HAL frame contract. + +## Tests / Validacao + +- `cargo test -p prometeu-hal` +- `rg "OwnedRgba8888Frame" crates/console/prometeu-hal -n` + +## Riscos + +- Reusing native pixel terminology could blur the logical RGBA8888 contract. +- Overfitting the frame type to current desktop upload code would leak host details into HAL. diff --git a/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md b/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md new file mode 100644 index 00000000..7d561d48 --- /dev/null +++ b/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md @@ -0,0 +1,79 @@ +--- +id: PLN-0111 +ticket: real-render-worker-establishment +title: Define Read-Only Render Resource Access +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, hal, resources] +--- + +## Briefing + +`DEC-0033` requires the worker backend to resolve render resources by stable ids through compact read-only access, without copying banks, snapshotting banks, requiring `Arc` in the public contract, or accessing mutable `Hardware`/`Gfx`/`FrameComposer`. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Define shared read-only render resource traits in HAL and implement them over existing local bank storage without changing bank ownership semantics. + +## Escopo + +- Define compact HAL traits for read-only glyph and scene bank access by id. +- Keep the public contract id-based and read-only. +- Implement the traits in the local driver/resource owner currently used by rendering. +- Add tests proving banks are read through the new interface. + +## Fora de Escopo + +- No worker thread. +- No bank copying or snapshotting. +- No public `Arc` requirement. +- No asset residency policy changes. + +## Plano de Execucao + +### Step 1 - Define HAL read-only traits + +**What:** Add worker-facing resource access contracts. +**How:** Define traits for resolving glyph and scene banks by stable ids. Return borrowed read-only references or compact read-only views, not mutable handles. +**File(s):** `crates/console/prometeu-hal/src/*`, likely a new render resource module. + +### Step 2 - Implement local resource access + +**What:** Expose current local bank storage through the new traits. +**How:** Implement the read-only traits for the existing memory bank/resource owner used by `prometeu-drivers`. +**File(s):** `crates/console/prometeu-drivers/src/memory_banks.rs`, `frame_composer.rs`, or adjacent resource modules. + +### Step 3 - Thread access through backend-facing APIs + +**What:** Make future backends able to receive resource access without `Hardware`. +**How:** Add narrow parameters/types where needed so a backend can be given read-only resources independently from the full platform. +**File(s):** HAL contracts and driver modules touched above. + +### Step 4 - Add resource access tests + +**What:** Prove id lookup and read-only behavior. +**How:** Add tests for resident/missing glyph and scene banks through the new traits and verify no mutable API is exposed. +**File(s):** HAL/driver tests. + +## Criterios de Aceite + +- [ ] Worker-facing render resource access is defined in `prometeu-hal`. +- [ ] Resource access is by stable id and read-only. +- [ ] The public contract does not require `Arc`, bank copies, bank snapshots, `Hardware`, `Gfx`, or `FrameComposer`. +- [ ] Local driver storage implements the traits. + +## Tests / Validacao + +- `cargo test -p prometeu-hal -p prometeu-drivers` +- `rg "Arc<.*RenderResource|snapshot.*bank|&mut Hardware|&mut Gfx|FrameComposer" crates/console/prometeu-hal crates/console/prometeu-drivers -n` + +## Riscos + +- Returning concrete bank internals may overexpose driver implementation. +- Accidentally requiring `Arc` in the trait would make a storage strategy part of the public contract. diff --git a/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md b/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md new file mode 100644 index 00000000..d9b498fa --- /dev/null +++ b/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md @@ -0,0 +1,78 @@ +--- +id: PLN-0112 +ticket: real-render-worker-establishment +title: Define Render Worker Errors and Telemetry +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, hal, telemetry] +--- + +## Briefing + +`DEC-0033` requires typed worker/backend errors and telemetry that observes worker behavior without changing VM semantics. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Introduce the shared error and telemetry vocabulary required by the real worker. + +## Escopo + +- Define `RenderWorkerError` or equivalent shared error type. +- Define worker telemetry counters/snapshot shape. +- Integrate the new telemetry shape with existing render telemetry types where appropriate. +- Add tests for error/debug/copy/equality behavior and telemetry defaults. + +## Fora de Escopo + +- No worker lifecycle implementation. +- No thread-safe handoff. +- No host integration. + +## Plano de Execucao + +### Step 1 - Add worker error type + +**What:** Define typed worker errors. +**How:** Add variants for backend unavailable, render failed, publish/present handoff failed, shutdown timeout, stale submission discarded, and worker panic/internal failure. +**File(s):** `crates/console/prometeu-hal/src/*` or `prometeu-system` if the type is not shared; prefer HAL if host/tests need it. + +### Step 2 - Add telemetry snapshot + +**What:** Define counters required by DEC-0033. +**How:** Include produced, replaced/dropped, consumed, published/presented, stale discarded, render failures, repeated frames, and shutdown timeouts. +**File(s):** existing telemetry/render manager modules or new worker telemetry module. + +### Step 3 - Preserve existing telemetry compatibility + +**What:** Avoid breaking existing render telemetry tests. +**How:** Map or extend current render telemetry without changing VM semantics or current counters unexpectedly. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs`, HAL telemetry modules as needed. + +### Step 4 - Add tests + +**What:** Pin type behavior. +**How:** Test default zero telemetry, counter increments through helper methods if added, and error formatting/debug behavior. +**File(s):** HAL/system tests. + +## Criterios de Aceite + +- [ ] Worker/backend failure has a typed error surface. +- [ ] Telemetry includes all DEC-0033 minimum counters. +- [ ] Existing render telemetry tests still pass. +- [ ] No worker/backend failure path relies on panic as the normal reporting model. + +## Tests / Validacao + +- `cargo test -p prometeu-hal -p prometeu-system` +- `rg "RenderWorkerError|shutdown_timeout|stale.*discard|repeat" crates/console -n` + +## Riscos + +- Splitting telemetry between too many structs may make final worker accounting inconsistent. +- Adding errors before call sites exist may create unused-code warnings if not scoped carefully. diff --git a/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md b/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md new file mode 100644 index 00000000..adffc62c --- /dev/null +++ b/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md @@ -0,0 +1,80 @@ +--- +id: PLN-0113 +ticket: real-render-worker-establishment +title: Implement Thread-Safe Latest-Wins Handoff +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, concurrency, handoff] +--- + +## Briefing + +`DEC-0033` requires a thread-safe single-slot latest-wins handoff. The producer must replace pending work without waiting for rasterization or present, and the consumer must take ownership of the latest pending submission. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Add the real worker handoff primitive in `prometeu-system`, using explicit synchronization and deterministic tests. + +## Escopo + +- Implement a single-slot pending submission structure. +- Use `Mutex> + Condvar` or an equivalent explicit structure. +- Add producer publish/replace behavior and consumer wait/take behavior. +- Track replacement/drop telemetry. + +## Fora de Escopo + +- No worker thread controller lifecycle. +- No backend rendering. +- No host integration. +- No stale epoch publish behavior beyond storing ownership data already present in submissions. + +## Plano de Execucao + +### Step 1 - Add handoff module + +**What:** Create the worker handoff primitive. +**How:** Add a module under `crates/console/prometeu-system/src/services/vm_runtime/`, for example `render_worker_handoff.rs`. +**File(s):** new system module, `mod.rs` exports. + +### Step 2 - Implement publish replacement + +**What:** Producer publishes owned `RenderSubmission`. +**How:** Lock the slot, replace any pending submission, increment replacement/drop counters when applicable, notify the condvar, and return immediately. +**File(s):** handoff module. + +### Step 3 - Implement consumer wait/take + +**What:** Consumer waits for work or shutdown signal. +**How:** Add wait/take APIs that block only the consumer side and return owned submissions. +**File(s):** handoff module. + +### Step 4 - Add deterministic tests + +**What:** Prove latest-wins and blocking boundaries. +**How:** Use threads, barriers, and condvars to prove producer replacement and consumer wake behavior without sleeps. +**File(s):** handoff module tests or `async_render_contract_tests.rs`. + +## Criterios de Aceite + +- [ ] Producer publish does not wait for consumer rasterization. +- [ ] Multiple pending publishes leave only the newest submission available. +- [ ] Replacements increment telemetry. +- [ ] Consumer can wait for work and is woken by publish. +- [ ] Tests do not use sleep as their primary synchronization proof. + +## Tests / Validacao + +- `cargo test -p prometeu-system render_worker_handoff` +- `cargo test -p prometeu-system` + +## Riscos + +- Holding the mutex while doing heavy work would violate non-blocking producer behavior. +- Condvar wait loops must handle spurious wakeups. diff --git a/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md b/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md new file mode 100644 index 00000000..065b48f4 --- /dev/null +++ b/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md @@ -0,0 +1,78 @@ +--- +id: PLN-0114 +ticket: real-render-worker-establishment +title: Add Deterministic Render Worker Test Harness +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, tests, concurrency] +--- + +## Briefing + +`DEC-0033` requires worker behavior to be proven without a native window and without fragile sleeps. This plan creates reusable deterministic test fixtures for the remaining worker plans. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Add a fake backend and synchronization harness capable of blocking render at controlled points, observing publication, and proving non-blocking VM/producer behavior. + +## Escopo + +- Create test-only synchronization helpers using barriers/condvars. +- Create a fake render backend shape that can block/fail/publish deterministically. +- Add helper functions for constructing owned render submissions and frames. +- Reuse the harness in later worker tests. + +## Fora de Escopo + +- No production worker lifecycle. +- No desktop host integration. +- No real GFX rasterization. + +## Plano de Execucao + +### Step 1 - Add test support module + +**What:** Introduce deterministic worker test fixtures. +**How:** Add test-only helpers under `prometeu-system` worker tests or a local test module. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/*`. + +### Step 2 - Add controllable fake backend + +**What:** Provide backend behavior for tests. +**How:** Fake backend can block at render start, unblock by test signal, return errors, and record published frames. +**File(s):** worker test module. + +### Step 3 - Add submission/frame builders + +**What:** Make tests concise and consistent. +**How:** Build Game/Shell submissions with explicit frame ids and ownership metadata; build `OwnedRgba8888Frame` fixtures. +**File(s):** worker test module. + +### Step 4 - Add initial harness self-tests + +**What:** Prove harness itself is deterministic. +**How:** Verify a test can block and release backend render without timing sleeps. +**File(s):** worker test module. + +## Criterios de Aceite + +- [ ] Tests can block worker backend render using deterministic synchronization. +- [ ] Tests can observe published `OwnedRgba8888Frame` values. +- [ ] Harness can inject backend errors. +- [ ] Harness helpers are scoped to tests or clearly internal. + +## Tests / Validacao + +- `cargo test -p prometeu-system render_worker` +- `rg "sleep\\(|thread::sleep" crates/console/prometeu-system/src/services/vm_runtime -n` + +## Riscos + +- Test harness code can become too coupled to implementation internals. +- Overly broad helpers can hide important assertions in later plans. diff --git a/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md b/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md new file mode 100644 index 00000000..509fe667 --- /dev/null +++ b/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md @@ -0,0 +1,87 @@ +--- +id: PLN-0115 +ticket: real-render-worker-establishment +title: Implement Render Worker Controller Lifecycle +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, lifecycle, concurrency] +--- + +## Briefing + +`DEC-0033` requires a runtime-owned worker controller in `prometeu-system`, with stop signaling, bounded shutdown, pending/in-flight separation, typed errors, and telemetry. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Implement the production worker controller lifecycle around the handoff primitive and backend trait shape. + +## Escopo + +- Add worker controller type in `prometeu-system`. +- Spawn and join a worker thread. +- Add configurable shutdown timeout with initial default `250ms`. +- Add stop token/wakeup behavior. +- Preserve pending/in-flight separation. +- Capture panic as typed internal failure. + +## Fora de Escopo + +- No desktop host presentation. +- No full runtime tick integration. +- No stale epoch/repeat behavior beyond lifecycle foundations. + +## Plano de Execucao + +### Step 1 - Define controller and config + +**What:** Add worker controller and configuration. +**How:** Include shutdown timeout, default `250ms`, and hooks for backend and handoff dependencies. +**File(s):** new `render_worker.rs` or similar in `prometeu-system`. + +### Step 2 - Implement start and run loop + +**What:** Start worker execution. +**How:** Spawn a thread that waits on the handoff, takes submissions, marks in-flight locally, calls backend, and publishes results through a frame sink abstraction. +**File(s):** worker controller module. + +### Step 3 - Implement stop and bounded join + +**What:** Make shutdown observable and bounded. +**How:** Signal stop, wake the condvar, join with timeout strategy available in Rust implementation, and return/report `ShutdownTimeout` when exceeded. +**File(s):** worker controller module. + +### Step 4 - Add panic containment + +**What:** Convert worker panic into typed failure. +**How:** Use panic containment around backend calls or worker thread join result and update telemetry/errors. +**File(s):** worker controller module. + +### Step 5 - Add lifecycle tests + +**What:** Prove start/stop/join behavior. +**How:** Use deterministic harness from `PLN-0114`. +**File(s):** worker tests. + +## Criterios de Aceite + +- [ ] Worker controller lives in `prometeu-system`. +- [ ] Controller can start and stop a real worker thread. +- [ ] Shutdown wakes waiting worker and completes within configured bound in tests. +- [ ] Shutdown timeout is typed and observable. +- [ ] Worker panic is captured as typed internal failure. + +## Tests / Validacao + +- `cargo test -p prometeu-system render_worker` +- `cargo test -p prometeu-system` + +## Riscos + +- Rust thread join timeout requires careful design because standard `JoinHandle::join` has no timeout. +- Backend ownership must be `'static + Send` without leaking host-specific types into system contracts. diff --git a/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md b/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md new file mode 100644 index 00000000..f1c94e45 --- /dev/null +++ b/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md @@ -0,0 +1,78 @@ +--- +id: PLN-0116 +ticket: real-render-worker-establishment +title: Implement Fake Local Render Backend +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, backend, drivers] +--- + +## Briefing + +`DEC-0033` requires fake/local backend behavior before desktop integration so worker semantics can be proven without a window. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Implement backend contracts that consume `RenderSubmission` plus read-only resources and produce `OwnedRgba8888Frame`. + +## Escopo + +- Define or finalize a backend trait for worker rendering. +- Add a fake backend for deterministic tests. +- Add a local framebuffer backend that can rasterize through existing driver/GFX logic without exposing mutable `Hardware` as the worker contract. +- Verify output frame ownership and pixels. + +## Fora de Escopo + +- No desktop upload/present. +- No runtime tick integration. +- No resource mutation policy changes. + +## Plano de Execucao + +### Step 1 - Define backend trait + +**What:** Encode backend consumption contract. +**How:** Trait consumes `RenderSubmission` and read-only resource access and returns `OwnedRgba8888Frame` or `RenderWorkerError`. +**File(s):** HAL if shared, or system if internal; prefer HAL if host/drivers implement it. + +### Step 2 - Implement fake backend + +**What:** Provide test backend. +**How:** Fake backend returns deterministic frames, can block through harness, and can return typed errors. +**File(s):** system test module or driver test utility. + +### Step 3 - Implement local framebuffer backend + +**What:** Provide non-window raster backend. +**How:** Use existing GFX rendering internally to turn submissions into `OwnedRgba8888Frame`, but expose only the backend trait and read-only resources. +**File(s):** `prometeu-drivers` rendering modules, possibly a new backend module. + +### Step 4 - Add output tests + +**What:** Prove frame content and metadata. +**How:** Render simple Game2D and ShellUi submissions and compare `OwnedRgba8888Frame` pixels/metadata. +**File(s):** driver/system tests. + +## Criterios de Aceite + +- [ ] Backend trait does not expose mutable `Hardware`, `Gfx`, `FrameComposer`, or VM state. +- [ ] Fake backend supports deterministic blocking/error behavior. +- [ ] Local backend produces `OwnedRgba8888Frame`. +- [ ] Game2D and ShellUi submissions can produce frame output without a native window. + +## Tests / Validacao + +- `cargo test -p prometeu-drivers -p prometeu-system` +- `rg "&mut Hardware|&mut Gfx|FrameComposer" crates/console/prometeu-hal crates/console/prometeu-system/src/services/vm_runtime -n` + +## Riscos + +- Local backend may be tempted to reuse `Hardware` directly as the public worker contract. +- Resource access gaps may surface when moving from fake to local rasterization. diff --git a/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md b/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md new file mode 100644 index 00000000..9fa5f8ea --- /dev/null +++ b/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md @@ -0,0 +1,80 @@ +--- +id: PLN-0117 +ticket: real-render-worker-establishment +title: Integrate Runtime Submission With Worker +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, integration] +--- + +## Briefing + +`DEC-0033` requires VM/render production to submit work without blocking on worker consumption. This plan connects runtime frame closure to the worker handoff while preserving local synchronous fallback. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Route eligible Game render submissions from `RenderManager` into the real worker handoff and keep Shell/local paths correct. + +## Escopo + +- Add worker-capable runtime path. +- Submit owned `RenderSubmission` to the handoff. +- Preserve AppMode policy: Game may use worker; Shell remains local unless a future decision changes it. +- Keep local synchronous fallback. +- Prove VM tick returns without waiting for worker backend completion. + +## Fora de Escopo + +- No desktop host presentation. +- No final stale/repeat behavior beyond integration needed for basic worker use. +- No spec edits. + +## Plano de Execucao + +### Step 1 - Add worker ownership to runtime state + +**What:** Store worker controller/handoff in runtime-owned state. +**How:** Extend `VirtualMachineRuntime` or `RenderManager` integration points without moving policy to host. +**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tick.rs`, `render_manager.rs`, worker modules. + +### Step 2 - Submit Game frames to worker handoff + +**What:** Replace local worker prototype path with real worker submission. +**How:** On eligible Game frame closure, publish owned `RenderSubmission` into the handoff and return to VM tick without waiting for consumption. +**File(s):** runtime tick/render manager integration. + +### Step 3 - Preserve local fallback + +**What:** Keep current local synchronous behavior available. +**How:** Use existing `RenderConsumerPath`/policy or update it to select local fallback when worker is disabled/unavailable. +**File(s):** `render_manager.rs`, `tick.rs`. + +### Step 4 - Add non-blocking tests + +**What:** Prove VM producer does not block. +**How:** Use harness to hold backend render, run tick/publish additional frames, assert return and replacement telemetry. +**File(s):** runtime worker tests. + +## Criterios de Aceite + +- [ ] Game render submissions can be sent to the real worker handoff. +- [ ] VM tick does not wait for worker rasterization. +- [ ] Shell/local policy remains unchanged. +- [ ] Local synchronous fallback still passes existing tests. +- [ ] Replacement telemetry increments when producer outruns worker. + +## Tests / Validacao + +- `cargo test -p prometeu-system` +- `cargo test -p prometeu-firmware` + +## Riscos + +- Accidentally making frame closure wait for worker consumption would violate DEC-0033. +- Mixing Shell lifecycle rendering into the worker path too early would broaden scope. diff --git a/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md b/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md new file mode 100644 index 00000000..343ac32d --- /dev/null +++ b/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md @@ -0,0 +1,85 @@ +--- +id: PLN-0118 +ticket: real-render-worker-establishment +title: Add Stale Epoch and Repeat Frame Behavior +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, ownership, telemetry] +--- + +## Briefing + +`DEC-0033` requires the worker to discard stale in-flight frames before publication and repeat the last published `OwnedRgba8888Frame` rather than recomposing. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Complete ownership/epoch checks and repeat-last-published-frame behavior for the worker path. + +## Escopo + +- Add shared active ownership/epoch observation for worker publication checks. +- Discard stale in-flight frames before publication. +- Store and expose the latest published `OwnedRgba8888Frame`. +- Implement repeat-last-frame behavior and telemetry. + +## Fora de Escopo + +- No desktop upload/present. +- No new AppMode policy. +- No native window invalidation changes. + +## Plano de Execucao + +### Step 1 - Expose ownership/epoch to worker + +**What:** Let worker validate in-flight submissions before publishing. +**How:** Add a thread-safe snapshot or handle that the worker can read without mutating runtime state. +**File(s):** `render_manager.rs`, worker modules. + +### Step 2 - Implement stale discard + +**What:** Prevent stale pixels from publication. +**How:** Before publishing frame output, compare submission ownership/epoch with active ownership/epoch; discard on mismatch and increment telemetry. +**File(s):** worker controller module. + +### Step 3 - Store latest published frame + +**What:** Make the latest valid frame available to host/fallback tests. +**How:** Add a published-frame slot distinct from pending submission and in-flight work. +**File(s):** worker controller or published frame module. + +### Step 4 - Implement repeat behavior + +**What:** Repeat last valid frame without recomposition. +**How:** Add API to fetch/reuse the latest published `OwnedRgba8888Frame`; increment repeat telemetry when cadence requests reuse. +**File(s):** worker modules, tests. + +### Step 5 - Add tests + +**What:** Prove stale discard and repeat. +**How:** Use harness to hold in-flight work, change owner/epoch, release backend, and assert no publication; separately assert repeat returns last frame. +**File(s):** worker tests. + +## Criterios de Aceite + +- [ ] Worker checks active owner/epoch before publishing. +- [ ] Stale in-flight frames are discarded and counted. +- [ ] Latest published frame is an `OwnedRgba8888Frame`. +- [ ] Repeat uses the last published frame and does not recompose. +- [ ] Tests cover stale discard and repeat deterministically. + +## Tests / Validacao + +- `cargo test -p prometeu-system render_worker` +- `cargo test -p prometeu-system` + +## Riscos + +- Reading active ownership from the wrong layer may reintroduce mutable runtime coupling. +- Repeat behavior must not hide render failures unless telemetry records them. diff --git a/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md b/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md new file mode 100644 index 00000000..2c97edd8 --- /dev/null +++ b/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md @@ -0,0 +1,81 @@ +--- +id: PLN-0119 +ticket: real-render-worker-establishment +title: Integrate Desktop Host Worker Presentation +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, host, desktop] +--- + +## Briefing + +`DEC-0033` is not complete until the real host path uses the worker. This plan integrates desktop presentation with the latest published `OwnedRgba8888Frame` while keeping native upload/present in the host event loop. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Make `prometeu-host-desktop-winit` present frames produced by the worker path. + +## Escopo + +- Wire the host to enable/use the worker path. +- Read the latest published `OwnedRgba8888Frame`. +- Upload/copy RGBA8888 pixels to the existing pixels/winit presentation surface in the event loop. +- Preserve host invalidation and redraw behavior. +- Keep worker free of native window dependencies. + +## Fora de Escopo + +- No SDL migration. +- No change to logical pixel format. +- No change to input/audio/filesystem host behavior. + +## Plano de Execucao + +### Step 1 - Add host access to published frame + +**What:** Expose latest worker-published frame to desktop host. +**How:** Add an API through runtime/platform integration that returns or copies the latest `OwnedRgba8888Frame` for host upload. +**File(s):** system worker modules, host runner integration. + +### Step 2 - Upload RGBA8888 in event loop + +**What:** Present worker output through existing host surface. +**How:** Convert/copy `Vec` RGBA8888 into the existing pixels frame buffer using existing RGBA8888 utility conventions. +**File(s):** `crates/host/prometeu-host-desktop-winit/src/runner.rs`, `utilities.rs` if needed. + +### Step 3 - Preserve invalidation model + +**What:** Keep host redraw policy coherent. +**How:** Trigger redraw when a new frame is published or host invalidation occurs; repeat uses the last published frame. +**File(s):** host runner/presentation state. + +### Step 4 - Add host tests + +**What:** Prove host consumes worker-published frame. +**How:** Add unit tests for RGBA8888 copy/upload helper and presentation state transitions without opening a native window. +**File(s):** host tests. + +## Criterios de Aceite + +- [ ] Desktop host presents the latest worker-published `OwnedRgba8888Frame`. +- [ ] Worker code does not import winit, pixels, SDL, swapchain, native texture, or window APIs. +- [ ] Host event loop remains responsible for native present. +- [ ] Existing host invalidation tests still pass. +- [ ] Real host path can run with worker enabled. + +## Tests / Validacao + +- `cargo test -p prometeu-host-desktop-winit` +- `cargo test -p prometeu-system -p prometeu-firmware` +- Manual run of a simple cartridge through desktop host with worker path enabled. + +## Riscos + +- Upload/copy can accidentally reinterpret RGBA8888 as native endian bytes. +- Host redraw policy can regress into perpetual polling if publication/invalidation are not kept separate. diff --git a/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md b/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md new file mode 100644 index 00000000..b87f770e --- /dev/null +++ b/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md @@ -0,0 +1,79 @@ +--- +id: PLN-0120 +ticket: real-render-worker-establishment +title: Update Real Render Worker Specs +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, specs] +--- + +## Briefing + +`DEC-0033` requires canonical runtime specs to document the real worker contract after implementation publishes the behavior. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Update English canonical specs to describe `OwnedRgba8888Frame`, worker handoff, host presentation split, read-only bank access, shutdown, errors, and telemetry. + +## Escopo + +- Update GFX/render specs. +- Update portability/host presentation specs. +- Update host ABI/syscall specs if worker behavior affects published runtime surfaces. +- Ensure no spec says the worker owns native window present. + +## Fora de Escopo + +- No new architecture beyond `DEC-0033`. +- No lesson writing. +- No implementation changes except documentation. + +## Plano de Execucao + +### Step 1 - Update GFX spec + +**What:** Document worker frame publication. +**How:** Add `OwnedRgba8888Frame`, RGBA8888 `Vec` semantics, latest published frame, and repeat behavior. +**File(s):** `docs/specs/runtime/04-gfx-peripheral.md`. + +### Step 2 - Update portability spec + +**What:** Document worker/host split. +**How:** State that worker produces owned RGBA8888 frames and host event loop performs native upload/present. +**File(s):** `docs/specs/runtime/11-portability-and-cross-platform-execution.md`. + +### Step 3 - Update concurrency/events spec if needed + +**What:** Document handoff and shutdown behavior. +**How:** Add single-slot latest-wins, bounded shutdown, and non-blocking producer semantics where concurrency is specified. +**File(s):** `docs/specs/runtime/09-events-and-concurrency.md` or adjacent runtime spec. + +### Step 4 - Update README/index references + +**What:** Keep spec map discoverable. +**How:** Add any needed cross-reference to worker/render sections. +**File(s):** `docs/specs/runtime/README.md`. + +## Criterios de Aceite + +- [ ] Specs define `OwnedRgba8888Frame`. +- [ ] Specs state worker does not own native window present. +- [ ] Specs state producer does not block on worker rasterization/present. +- [ ] Specs document read-only bank access by id. +- [ ] Specs document bounded shutdown and typed error expectations. + +## Tests / Validacao + +- `rg "OwnedRgba8888Frame|latest-wins|shutdown|read-only" docs/specs/runtime -n` +- `discussion validate` + +## Riscos + +- Updating specs before implementation may accidentally publish intent as completed behavior; execute after code plans that establish the behavior. +- Spec wording must remain English and normative. diff --git a/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md b/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md new file mode 100644 index 00000000..40fa25cf --- /dev/null +++ b/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md @@ -0,0 +1,93 @@ +--- +id: PLN-0121 +ticket: real-render-worker-establishment +title: Final Worker Path Validation and Hardening +status: open +created: 2026-06-15 +completed: +ref_decisions: [DEC-0033] +tags: [runtime, renderer, worker, validation, hardening] +--- + +## Briefing + +`DEC-0033` is complete only when the worker functions on the real host path and the contract is proven end to end. This plan performs final validation, residue scans, and hardening before housekeeping. + +## Decisions de Origem + +- `DEC-0033`: Real Render Worker Contract. + +## Alvo + +Prove the real render worker path satisfies DEC-0033 across HAL, system, drivers, firmware, host, specs, and tests. + +## Escopo + +- Run broad test suites. +- Add missing edge tests found during validation. +- Scan for forbidden coupling. +- Verify host real path uses worker. +- Verify local fallback still works. +- Prepare evidence for discussion housekeeping. + +## Fora de Escopo + +- No new worker architecture. +- No SDL migration. +- No unrelated performance tuning. + +## Plano de Execucao + +### Step 1 - Run full affected test suite + +**What:** Validate all touched crates. +**How:** Run workspace tests and focused worker/host tests. +**File(s):** repository root. + +### Step 2 - Run coupling residue scans + +**What:** Enforce DEC-0033 boundaries. +**How:** Search worker/system/HAL code for native window dependencies, mutable hardware/GFX/composer leaks, bank copying/snapshot terminology, and sleeps in concurrency tests. +**File(s):** repository root. + +### Step 3 - Add missing hardening tests + +**What:** Close any discovered gaps. +**How:** Add focused tests for missing telemetry/error/shutdown/host-path cases. +**File(s):** affected test modules. + +### Step 4 - Verify host path + +**What:** Prove desktop host uses worker output. +**How:** Run host tests and a manual/simple cartridge path with worker enabled; record evidence in final summary. +**File(s):** host runner and runtime integration. + +### Step 5 - Prepare housekeeping evidence + +**What:** Make `DSC-0042` ready for final lesson/housekeeping. +**How:** Record completed plan state and validation evidence for later `discussion-housekeep`. +**File(s):** discussion artifacts if needed. + +## Criterios de Aceite + +- [ ] `cargo test --workspace` passes. +- [ ] Focused worker, system, drivers, firmware, and host tests pass. +- [ ] Residue scans show worker code does not depend on native window APIs. +- [ ] Residue scans show worker boundary does not expose `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state. +- [ ] Desktop host path uses the real worker output. +- [ ] Local fallback still works. +- [ ] `discussion validate` passes. + +## Tests / Validacao + +- `cargo test --workspace` +- `cargo test -p prometeu-system -p prometeu-drivers -p prometeu-hal -p prometeu-firmware -p prometeu-host-desktop-winit` +- `discussion validate` +- `rg "winit|pixels|SDL|swapchain|native texture" crates/console/prometeu-system crates/console/prometeu-hal -n` +- `rg "&mut Hardware|&mut Gfx|FrameComposer" crates/console/prometeu-system/src/services/vm_runtime crates/console/prometeu-hal -n` +- `rg "thread::sleep|sleep\\(" crates/console/prometeu-system/src/services/vm_runtime -n` + +## Riscos + +- Final validation may expose earlier plan gaps; fix them in the narrowest affected plan/module. +- Manual host evidence can be flaky if tied to native window availability; keep automated host-unit evidence as the primary gate. From 12d314a62d87ccfe66072a5e0364857ef6b48fb3 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 16:01:47 +0100 Subject: [PATCH 35/47] implements PLN-0110 --- crates/console/prometeu-hal/src/lib.rs | 2 + .../console/prometeu-hal/src/owned_frame.rs | 127 ++++++++++++++++++ discussion/index.ndjson | 2 +- ...10-define-owned-rgba8888-frame-contract.md | 2 +- 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 crates/console/prometeu-hal/src/owned_frame.rs diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index 0c408783..71cc304b 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -17,6 +17,7 @@ pub mod input_signals; pub mod log; pub mod native_helpers; pub mod native_interface; +pub mod owned_frame; pub mod pad_bridge; pub mod platform; pub mod primitives; @@ -44,6 +45,7 @@ pub use host_return::HostReturn; pub use input_signals::InputSignals; pub use native_helpers::{expect_bool, expect_int}; pub use native_interface::{NativeInterface, SyscallId}; +pub use owned_frame::{OwnedRgba8888Frame, OwnedRgba8888FrameError}; pub use pad_bridge::PadBridge; pub use platform::{ AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform, diff --git a/crates/console/prometeu-hal/src/owned_frame.rs b/crates/console/prometeu-hal/src/owned_frame.rs new file mode 100644 index 00000000..d6b63a9f --- /dev/null +++ b/crates/console/prometeu-hal/src/owned_frame.rs @@ -0,0 +1,127 @@ +use crate::render_submission::{FrameId, RenderOwnership}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OwnedRgba8888Frame { + pub frame_id: FrameId, + pub ownership: RenderOwnership, + pub width: usize, + pub height: usize, + pub stride_pixels: usize, + pub pixels: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OwnedRgba8888FrameError { + EmptyDimensions, + StrideTooSmall, + PixelBufferTooSmall, +} + +impl OwnedRgba8888Frame { + pub fn new( + frame_id: FrameId, + ownership: RenderOwnership, + width: usize, + height: usize, + stride_pixels: usize, + pixels: Vec, + ) -> Result { + if width == 0 || height == 0 { + return Err(OwnedRgba8888FrameError::EmptyDimensions); + } + if stride_pixels < width { + return Err(OwnedRgba8888FrameError::StrideTooSmall); + } + let required_len = stride_pixels + .checked_mul(height) + .ok_or(OwnedRgba8888FrameError::PixelBufferTooSmall)?; + if pixels.len() < required_len { + return Err(OwnedRgba8888FrameError::PixelBufferTooSmall); + } + + Ok(Self { frame_id, ownership, width, height, stride_pixels, pixels }) + } + + pub fn packed( + frame_id: FrameId, + ownership: RenderOwnership, + width: usize, + height: usize, + pixels: Vec, + ) -> Result { + Self::new(frame_id, ownership, width, height, width, pixels) + } + + pub fn required_pixel_len(&self) -> usize { + self.stride_pixels * self.height + } + + pub fn row(&self, y: usize) -> Option<&[u32]> { + if y >= self.height { + return None; + } + + let start = y * self.stride_pixels; + Some(&self.pixels[start..start + self.width]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app_mode::AppMode; + + fn ownership() -> RenderOwnership { + RenderOwnership::new(11, AppMode::Game, 42) + } + + #[test] + fn owned_rgba8888_frame_preserves_metadata_and_pixels() { + let pixels = vec![0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00]; + let frame = OwnedRgba8888Frame::packed(FrameId::new(7), ownership(), 2, 2, pixels.clone()) + .expect("valid packed RGBA8888 frame"); + + assert_eq!(frame.frame_id, FrameId::new(7)); + assert_eq!(frame.ownership, ownership()); + assert_eq!(frame.width, 2); + assert_eq!(frame.height, 2); + assert_eq!(frame.stride_pixels, 2); + assert_eq!(frame.pixels, pixels); + assert_eq!(frame.required_pixel_len(), 4); + } + + #[test] + fn checked_constructor_rejects_empty_dimensions() { + let err = OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 0, 1, 1, vec![0]) + .expect_err("zero width should be rejected"); + + assert_eq!(err, OwnedRgba8888FrameError::EmptyDimensions); + } + + #[test] + fn checked_constructor_rejects_stride_smaller_than_width() { + let err = OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 3, 1, 2, vec![0, 0, 0]) + .expect_err("stride smaller than width should be rejected"); + + assert_eq!(err, OwnedRgba8888FrameError::StrideTooSmall); + } + + #[test] + fn checked_constructor_rejects_short_pixel_buffer() { + let err = OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 2, 2, 3, vec![0; 5]) + .expect_err("buffer shorter than stride * height should be rejected"); + + assert_eq!(err, OwnedRgba8888FrameError::PixelBufferTooSmall); + } + + #[test] + fn row_returns_visible_width_without_stride_padding() { + let frame = + OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 2, 2, 3, vec![1, 2, 99, 3, 4, 88]) + .expect("valid padded frame"); + + assert_eq!(frame.row(0), Some(&[1, 2][..])); + assert_eq!(frame.row(1), Some(&[3, 4][..])); + assert_eq!(frame.row(2), None); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index aef7f6c9..8aa2e25f 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md b/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md index 8cffebcb..77e020d7 100644 --- a/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md +++ b/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md @@ -2,7 +2,7 @@ id: PLN-0110 ticket: real-render-worker-establishment title: Define Owned RGBA8888 Frame Contract -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From 4c48b92f3ba36ff144f3deb758e82fada8b08d19 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 16:04:36 +0100 Subject: [PATCH 36/47] implements PLN-0111 --- crates/console/prometeu-drivers/src/lib.rs | 4 +- .../prometeu-drivers/src/memory_banks.rs | 62 ++++++---- crates/console/prometeu-hal/src/lib.rs | 2 + .../prometeu-hal/src/render_resources.rs | 109 ++++++++++++++++++ discussion/index.ndjson | 2 +- ...define-read-only-render-resource-access.md | 2 +- 6 files changed, 153 insertions(+), 28 deletions(-) create mode 100644 crates/console/prometeu-hal/src/render_resources.rs diff --git a/crates/console/prometeu-drivers/src/lib.rs b/crates/console/prometeu-drivers/src/lib.rs index dacd2eb2..c8fe4afd 100644 --- a/crates/console/prometeu-drivers/src/lib.rs +++ b/crates/console/prometeu-drivers/src/lib.rs @@ -13,8 +13,8 @@ pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE pub use crate::frame_composer::{ComposerBuffer, FrameComposer, SceneStatus, SpriteController}; pub use crate::gfx::Gfx; pub use crate::memory_banks::{ - GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, RenderResourceAccess, - SceneBankPoolAccess, SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller, + GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess, + SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller, }; pub use crate::pad::Pad; pub use crate::test_platform::TestPlatform; diff --git a/crates/console/prometeu-drivers/src/memory_banks.rs b/crates/console/prometeu-drivers/src/memory_banks.rs index 5cc1bc9c..a245a512 100644 --- a/crates/console/prometeu-drivers/src/memory_banks.rs +++ b/crates/console/prometeu-drivers/src/memory_banks.rs @@ -2,11 +2,12 @@ //! //! Render consumers must cross this boundary through small, read-only traits. //! Frame packets carry resource IDs; large resident resources stay in banks and -//! are returned as `Arc` snapshots of the slot contents. Installation and slot -//! replacement remain separate owner-side operations and are not part of the -//! render read surface. +//! are read in place through a narrow HAL callback contract. Installation and +//! slot replacement remain separate owner-side operations and are not part of +//! the render read surface. use prometeu_hal::glyph_bank::GlyphBank; +use prometeu_hal::render_resources::RenderResourceAccess as HalRenderResourceAccess; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::sound_bank::SoundBank; use std::sync::{Arc, RwLock}; @@ -53,15 +54,6 @@ pub trait SceneBankPoolInstaller: Send + Sync { fn install_scene_bank(&self, slot: usize, bank: Arc); } -/// Read-only resource surface needed by render consumers. -/// -/// This is intentionally narrower than `MemoryBanks`: render code can resolve -/// stable packet IDs to resident glyph/scene banks, but cannot install or swap -/// resources through this trait. -pub trait RenderResourceAccess: GlyphBankPoolAccess + SceneBankPoolAccess {} - -impl RenderResourceAccess for T where T: GlyphBankPoolAccess + SceneBankPoolAccess {} - /// Centralized container for all hardware memory banks. /// /// MemoryBanks represent the actual hardware slot state. @@ -150,6 +142,28 @@ impl SceneBankPoolInstaller for MemoryBanks { } } +impl HalRenderResourceAccess for MemoryBanks { + fn with_glyph_bank(&self, bank_id: usize, read: impl FnOnce(&GlyphBank) -> R) -> Option { + let pool = self.glyph_bank_pool.read().unwrap(); + let bank = pool.get(bank_id)?.as_deref()?; + Some(read(bank)) + } + + fn glyph_bank_count(&self) -> usize { + 16 + } + + fn with_scene_bank(&self, bank_id: usize, read: impl FnOnce(&SceneBank) -> R) -> Option { + let pool = self.scene_bank_pool.read().unwrap(); + let bank = pool.get(bank_id)?.as_deref()?; + Some(read(bank)) + } + + fn scene_bank_count(&self) -> usize { + 16 + } +} + #[cfg(test)] mod tests { use super::*; @@ -193,24 +207,24 @@ mod tests { assert_send_sync::(); assert_send_sync::>(); assert_send_sync::>(); - assert_send_sync::>(); } #[test] - fn render_resource_access_exposes_read_only_glyph_and_scene_banks() { - let banks = Arc::new(MemoryBanks::new()); + fn render_resource_access_exposes_read_only_glyph_and_scene_banks_without_arc_contract() { + let banks = MemoryBanks::new(); banks.install_glyph_bank(0, Arc::new(make_glyph_bank())); banks.install_scene_bank(1, Arc::new(make_scene_bank())); - let render_resources: Arc = banks; - let glyph_bank = - render_resources.glyph_bank_slot(0).expect("glyph bank slot should be readable"); - let scene_bank = - render_resources.scene_bank_slot(1).expect("scene bank slot should be readable"); + let glyph_tile_size = + HalRenderResourceAccess::with_glyph_bank(&banks, 0, |bank| bank.tile_size); + let scene_tile_size = + HalRenderResourceAccess::with_scene_bank(&banks, 1, |bank| bank.layers[0].tile_size); - assert_eq!(glyph_bank.tile_size, TileSize::Size8); - assert_eq!(scene_bank.layers[0].tile_size, TileSize::Size8); - assert!(render_resources.glyph_bank_slot(15).is_none()); - assert!(render_resources.scene_bank_slot(15).is_none()); + assert_eq!(glyph_tile_size, Some(TileSize::Size8)); + assert_eq!(scene_tile_size, Some(TileSize::Size8)); + assert_eq!(HalRenderResourceAccess::with_glyph_bank(&banks, 15, |_| ()), None); + assert_eq!(HalRenderResourceAccess::with_scene_bank(&banks, 15, |_| ()), None); + assert_eq!(HalRenderResourceAccess::glyph_bank_count(&banks), 16); + assert_eq!(HalRenderResourceAccess::scene_bank_count(&banks), 16); } } diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index 71cc304b..00a8931e 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -21,6 +21,7 @@ pub mod owned_frame; pub mod pad_bridge; pub mod platform; pub mod primitives; +pub mod render_resources; pub mod render_submission; pub mod sample; pub mod scene_bank; @@ -52,6 +53,7 @@ pub use platform::{ NoopTelemetryPlatform, RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform, TelemetryPlatform, TestPlatform, }; +pub use render_resources::RenderResourceAccess; pub use render_submission::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket, Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderOwnership, RenderSubmission, diff --git a/crates/console/prometeu-hal/src/render_resources.rs b/crates/console/prometeu-hal/src/render_resources.rs new file mode 100644 index 00000000..911070f3 --- /dev/null +++ b/crates/console/prometeu-hal/src/render_resources.rs @@ -0,0 +1,109 @@ +use crate::glyph_bank::GlyphBank; +use crate::scene_bank::SceneBank; + +/// Read-only render resource lookup used by render backends. +/// +/// Resource ids are stable bank slots carried by render submissions. The +/// callback shape keeps bank ownership inside the provider while exposing only +/// borrowed read access to the caller. +pub trait RenderResourceAccess { + /// Reads a resident glyph bank by id. + fn with_glyph_bank(&self, bank_id: usize, read: impl FnOnce(&GlyphBank) -> R) -> Option; + + /// Returns the number of glyph bank ids accepted by this provider. + fn glyph_bank_count(&self) -> usize; + + /// Reads a resident scene bank by id. + fn with_scene_bank(&self, bank_id: usize, read: impl FnOnce(&SceneBank) -> R) -> Option; + + /// Returns the number of scene bank ids accepted by this provider. + fn scene_bank_count(&self) -> usize; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::glyph::Glyph; + use crate::glyph_bank::TileSize; + use crate::scene_layer::{ParallaxFactor, SceneLayer}; + use crate::tile::Tile; + use crate::tilemap::TileMap; + + struct TestRenderResources { + glyph_banks: Vec>, + scene_banks: Vec>, + } + + impl RenderResourceAccess for TestRenderResources { + fn with_glyph_bank( + &self, + bank_id: usize, + read: impl FnOnce(&GlyphBank) -> R, + ) -> Option { + self.glyph_banks.get(bank_id)?.as_ref().map(read) + } + + fn glyph_bank_count(&self) -> usize { + self.glyph_banks.len() + } + + fn with_scene_bank( + &self, + bank_id: usize, + read: impl FnOnce(&SceneBank) -> R, + ) -> Option { + self.scene_banks.get(bank_id)?.as_ref().map(read) + } + + fn scene_bank_count(&self) -> usize { + self.scene_banks.len() + } + } + + fn scene_bank(tile_size: TileSize) -> SceneBank { + let layer = SceneLayer { + active: true, + glyph_asset_id: 7, + tile_size, + parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, + tilemap: TileMap { + width: 1, + height: 1, + tiles: vec![Tile { + active: true, + glyph: Glyph { glyph_id: 3, palette_id: 0 }, + flip_x: false, + flip_y: false, + }], + }, + }; + + SceneBank { layers: std::array::from_fn(|_| layer.clone()) } + } + + #[test] + fn reads_resident_resources_by_id() { + let resources = TestRenderResources { + glyph_banks: vec![None, Some(GlyphBank::new(TileSize::Size16, 16, 16)), None], + scene_banks: vec![Some(scene_bank(TileSize::Size8)), None], + }; + + assert_eq!(resources.glyph_bank_count(), 3); + assert_eq!(resources.scene_bank_count(), 2); + assert_eq!(resources.with_glyph_bank(1, |bank| bank.tile_size), Some(TileSize::Size16)); + assert_eq!( + resources.with_scene_bank(0, |bank| bank.layers[0].tile_size), + Some(TileSize::Size8) + ); + } + + #[test] + fn missing_resources_return_none() { + let resources = TestRenderResources { glyph_banks: vec![None], scene_banks: vec![None] }; + + assert_eq!(resources.with_glyph_bank(0, |bank| bank.width), None); + assert_eq!(resources.with_glyph_bank(9, |bank| bank.width), None); + assert_eq!(resources.with_scene_bank(0, |bank| bank.layers[0].active), None); + assert_eq!(resources.with_scene_bank(9, |bank| bank.layers[0].active), None); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 8aa2e25f..91e7066c 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md b/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md index 7d561d48..469ab083 100644 --- a/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md +++ b/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md @@ -2,7 +2,7 @@ id: PLN-0111 ticket: real-render-worker-establishment title: Define Read-Only Render Resource Access -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From 57bf2b84d3e0a515e2beae99f24ffbf46f475594 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 16:06:53 +0100 Subject: [PATCH 37/47] implements PLN-0112 --- crates/console/prometeu-hal/src/lib.rs | 2 + .../console/prometeu-hal/src/render_worker.rs | 190 ++++++++++++++++++ crates/console/prometeu-hal/src/telemetry.rs | 94 +++++++++ discussion/index.ndjson | 2 +- ...fine-render-worker-errors-and-telemetry.md | 2 +- 5 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 crates/console/prometeu-hal/src/render_worker.rs diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index 00a8931e..62c6863d 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -23,6 +23,7 @@ pub mod platform; pub mod primitives; pub mod render_resources; pub mod render_submission; +pub mod render_worker; pub mod sample; pub mod scene_bank; pub mod scene_layer; @@ -59,4 +60,5 @@ pub use render_submission::{ Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderOwnership, RenderSubmission, RenderSubmissionPacket, ShellUiFramePacket, }; +pub use render_worker::{RenderWorkerError, RenderWorkerTelemetry}; pub use touch_bridge::TouchBridge; diff --git a/crates/console/prometeu-hal/src/render_worker.rs b/crates/console/prometeu-hal/src/render_worker.rs new file mode 100644 index 00000000..44ebbc60 --- /dev/null +++ b/crates/console/prometeu-hal/src/render_worker.rs @@ -0,0 +1,190 @@ +use std::fmt; + +/// Typed failure surface for the real render worker and its backend. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderWorkerError { + BackendUnavailable, + RenderFailed, + PublishFailed, + PresentFailed, + ShutdownTimeout, + StaleSubmissionDiscarded, + WorkerPanic, + InternalFailure, +} + +impl fmt::Display for RenderWorkerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BackendUnavailable => f.write_str("render backend unavailable"), + Self::RenderFailed => f.write_str("render failed"), + Self::PublishFailed => f.write_str("render publish failed"), + Self::PresentFailed => f.write_str("render present failed"), + Self::ShutdownTimeout => f.write_str("render worker shutdown timed out"), + Self::StaleSubmissionDiscarded => f.write_str("stale render submission discarded"), + Self::WorkerPanic => f.write_str("render worker panicked"), + Self::InternalFailure => f.write_str("internal render worker failure"), + } + } +} + +impl std::error::Error for RenderWorkerError {} + +/// Snapshot of render worker handoff, render, publish, and shutdown counters. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct RenderWorkerTelemetry { + pub produced_submissions: u64, + pub replaced_before_consume: u64, + pub consumed_submissions: u64, + pub published_frames: u64, + pub presented_frames: u64, + pub repeated_frames: u64, + pub stale_submission_discards: u64, + pub render_failures: u64, + pub publish_failures: u64, + pub present_failures: u64, + pub shutdown_timeouts: u64, + pub worker_panics: u64, + pub last_produced_frame_id: u64, + pub last_consumed_frame_id: u64, + pub last_published_frame_id: u64, + pub last_presented_frame_id: u64, + pub last_dropped_frame_id: u64, + pub last_error_frame_id: u64, + pub active_render_epoch: u64, +} + +impl RenderWorkerTelemetry { + pub fn record_produced(&mut self, frame_id: u64) { + self.produced_submissions += 1; + self.last_produced_frame_id = frame_id; + } + + pub fn record_replaced_before_consume(&mut self, frame_id: u64) { + self.replaced_before_consume += 1; + self.last_dropped_frame_id = frame_id; + } + + pub fn record_consumed(&mut self, frame_id: u64) { + self.consumed_submissions += 1; + self.last_consumed_frame_id = frame_id; + } + + pub fn record_published(&mut self, frame_id: u64) { + self.published_frames += 1; + self.last_published_frame_id = frame_id; + } + + pub fn record_presented(&mut self, frame_id: u64) { + self.presented_frames += 1; + self.last_presented_frame_id = frame_id; + } + + pub fn record_repeated_frame(&mut self, frame_id: u64) { + self.repeated_frames += 1; + self.last_presented_frame_id = frame_id; + } + + pub fn record_stale_submission_discarded(&mut self, frame_id: u64) { + self.stale_submission_discards += 1; + self.last_dropped_frame_id = frame_id; + } + + pub fn record_error(&mut self, error: RenderWorkerError, frame_id: u64) { + match error { + RenderWorkerError::BackendUnavailable | RenderWorkerError::RenderFailed => { + self.render_failures += 1; + } + RenderWorkerError::PublishFailed => { + self.publish_failures += 1; + } + RenderWorkerError::PresentFailed => { + self.present_failures += 1; + } + RenderWorkerError::ShutdownTimeout => { + self.shutdown_timeouts += 1; + } + RenderWorkerError::WorkerPanic => { + self.worker_panics += 1; + } + RenderWorkerError::StaleSubmissionDiscarded => { + self.stale_submission_discards += 1; + self.last_dropped_frame_id = frame_id; + } + RenderWorkerError::InternalFailure => { + self.render_failures += 1; + } + } + self.last_error_frame_id = frame_id; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_worker_error_is_copy_equatable_and_debuggable() { + let error = RenderWorkerError::ShutdownTimeout; + let copied = error; + + assert_eq!(copied, RenderWorkerError::ShutdownTimeout); + assert_eq!(format!("{:?}", error), "ShutdownTimeout"); + assert_eq!(error.to_string(), "render worker shutdown timed out"); + } + + #[test] + fn render_worker_telemetry_defaults_to_zero() { + assert_eq!( + RenderWorkerTelemetry::default(), + RenderWorkerTelemetry { + produced_submissions: 0, + replaced_before_consume: 0, + consumed_submissions: 0, + published_frames: 0, + presented_frames: 0, + repeated_frames: 0, + stale_submission_discards: 0, + render_failures: 0, + publish_failures: 0, + present_failures: 0, + shutdown_timeouts: 0, + worker_panics: 0, + last_produced_frame_id: 0, + last_consumed_frame_id: 0, + last_published_frame_id: 0, + last_presented_frame_id: 0, + last_dropped_frame_id: 0, + last_error_frame_id: 0, + active_render_epoch: 0, + } + ); + } + + #[test] + fn render_worker_telemetry_records_counters_and_frame_ids() { + let mut telemetry = RenderWorkerTelemetry::default(); + + telemetry.record_produced(10); + telemetry.record_replaced_before_consume(10); + telemetry.record_consumed(11); + telemetry.record_published(11); + telemetry.record_presented(11); + telemetry.record_repeated_frame(11); + telemetry.record_error(RenderWorkerError::PublishFailed, 12); + telemetry.record_error(RenderWorkerError::WorkerPanic, 13); + telemetry.record_stale_submission_discarded(14); + + assert_eq!(telemetry.produced_submissions, 1); + assert_eq!(telemetry.replaced_before_consume, 1); + assert_eq!(telemetry.consumed_submissions, 1); + assert_eq!(telemetry.published_frames, 1); + assert_eq!(telemetry.presented_frames, 1); + assert_eq!(telemetry.repeated_frames, 1); + assert_eq!(telemetry.publish_failures, 1); + assert_eq!(telemetry.worker_panics, 1); + assert_eq!(telemetry.stale_submission_discards, 1); + assert_eq!(telemetry.last_error_frame_id, 13); + assert_eq!(telemetry.last_dropped_frame_id, 14); + } +} diff --git a/crates/console/prometeu-hal/src/telemetry.rs b/crates/console/prometeu-hal/src/telemetry.rs index 17253ac7..405e3f1f 100644 --- a/crates/console/prometeu-hal/src/telemetry.rs +++ b/crates/console/prometeu-hal/src/telemetry.rs @@ -1,4 +1,5 @@ use crate::log::{LogLevel, LogService, LogSource}; +use crate::render_worker::RenderWorkerTelemetry; use std::sync::Arc; use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering}; @@ -34,20 +35,51 @@ pub struct TelemetryFrame { pub produced_submissions: u64, pub replaced_before_consume: u64, pub consumed_submissions: u64, + pub published_frames: u64, pub presented_frames: u64, pub repeated_presents: u64, pub render_errors: u64, + pub publish_errors: u64, pub present_errors: u64, pub stale_epoch_discards: u64, pub shutdown_discards: u64, + pub shutdown_timeouts: u64, + pub worker_panics: u64, pub last_produced_frame_id: u64, pub last_consumed_frame_id: u64, + pub last_published_frame_id: u64, pub last_presented_frame_id: u64, pub last_dropped_frame_id: u64, pub last_error_frame_id: u64, pub active_render_epoch: u64, } +impl TelemetryFrame { + pub fn render_worker_telemetry(&self) -> RenderWorkerTelemetry { + RenderWorkerTelemetry { + produced_submissions: self.produced_submissions, + replaced_before_consume: self.replaced_before_consume, + consumed_submissions: self.consumed_submissions, + published_frames: self.published_frames, + presented_frames: self.presented_frames, + repeated_frames: self.repeated_presents, + stale_submission_discards: self.stale_epoch_discards, + render_failures: self.render_errors, + publish_failures: self.publish_errors, + present_failures: self.present_errors, + shutdown_timeouts: self.shutdown_timeouts, + worker_panics: self.worker_panics, + last_produced_frame_id: self.last_produced_frame_id, + last_consumed_frame_id: self.last_consumed_frame_id, + last_published_frame_id: self.last_published_frame_id, + last_presented_frame_id: self.last_presented_frame_id, + last_dropped_frame_id: self.last_dropped_frame_id, + last_error_frame_id: self.last_error_frame_id, + active_render_epoch: self.active_render_epoch, + } + } +} + /// Thread-safe, atomic telemetry storage for real-time monitoring by the host. /// This follows the push-based model from DEC-0005 to avoid expensive scans or locks. #[derive(Debug, Default)] @@ -84,14 +116,19 @@ pub struct AtomicTelemetry { pub produced_submissions: AtomicU64, pub replaced_before_consume: AtomicU64, pub consumed_submissions: AtomicU64, + pub published_frames: AtomicU64, pub presented_frames: AtomicU64, pub repeated_presents: AtomicU64, pub render_errors: AtomicU64, + pub publish_errors: AtomicU64, pub present_errors: AtomicU64, pub stale_epoch_discards: AtomicU64, pub shutdown_discards: AtomicU64, + pub shutdown_timeouts: AtomicU64, + pub worker_panics: AtomicU64, pub last_produced_frame_id: AtomicU64, pub last_consumed_frame_id: AtomicU64, + pub last_published_frame_id: AtomicU64, pub last_presented_frame_id: AtomicU64, pub last_dropped_frame_id: AtomicU64, pub last_error_frame_id: AtomicU64, @@ -128,14 +165,19 @@ impl AtomicTelemetry { produced_submissions: self.produced_submissions.load(Ordering::Relaxed), replaced_before_consume: self.replaced_before_consume.load(Ordering::Relaxed), consumed_submissions: self.consumed_submissions.load(Ordering::Relaxed), + published_frames: self.published_frames.load(Ordering::Relaxed), presented_frames: self.presented_frames.load(Ordering::Relaxed), repeated_presents: self.repeated_presents.load(Ordering::Relaxed), render_errors: self.render_errors.load(Ordering::Relaxed), + publish_errors: self.publish_errors.load(Ordering::Relaxed), present_errors: self.present_errors.load(Ordering::Relaxed), stale_epoch_discards: self.stale_epoch_discards.load(Ordering::Relaxed), shutdown_discards: self.shutdown_discards.load(Ordering::Relaxed), + shutdown_timeouts: self.shutdown_timeouts.load(Ordering::Relaxed), + worker_panics: self.worker_panics.load(Ordering::Relaxed), last_produced_frame_id: self.last_produced_frame_id.load(Ordering::Relaxed), last_consumed_frame_id: self.last_consumed_frame_id.load(Ordering::Relaxed), + last_published_frame_id: self.last_published_frame_id.load(Ordering::Relaxed), last_presented_frame_id: self.last_presented_frame_id.load(Ordering::Relaxed), last_dropped_frame_id: self.last_dropped_frame_id.load(Ordering::Relaxed), last_error_frame_id: self.last_error_frame_id.load(Ordering::Relaxed), @@ -165,14 +207,19 @@ impl AtomicTelemetry { self.produced_submissions.store(0, Ordering::Relaxed); self.replaced_before_consume.store(0, Ordering::Relaxed); self.consumed_submissions.store(0, Ordering::Relaxed); + self.published_frames.store(0, Ordering::Relaxed); self.presented_frames.store(0, Ordering::Relaxed); self.repeated_presents.store(0, Ordering::Relaxed); self.render_errors.store(0, Ordering::Relaxed); + self.publish_errors.store(0, Ordering::Relaxed); self.present_errors.store(0, Ordering::Relaxed); self.stale_epoch_discards.store(0, Ordering::Relaxed); self.shutdown_discards.store(0, Ordering::Relaxed); + self.shutdown_timeouts.store(0, Ordering::Relaxed); + self.worker_panics.store(0, Ordering::Relaxed); self.last_produced_frame_id.store(0, Ordering::Relaxed); self.last_consumed_frame_id.store(0, Ordering::Relaxed); + self.last_published_frame_id.store(0, Ordering::Relaxed); self.last_presented_frame_id.store(0, Ordering::Relaxed); self.last_dropped_frame_id.store(0, Ordering::Relaxed); self.last_error_frame_id.store(0, Ordering::Relaxed); @@ -398,4 +445,51 @@ mod tests { assert_eq!(snapshot.vm_heap_allocations, 2); assert_eq!(snapshot.vm_string_materializations, 5); } + + #[test] + fn snapshot_maps_render_worker_telemetry() { + let current = Arc::new(AtomicU32::new(0)); + let tel = AtomicTelemetry::new(current); + tel.produced_submissions.store(2, Ordering::Relaxed); + tel.replaced_before_consume.store(1, Ordering::Relaxed); + tel.consumed_submissions.store(1, Ordering::Relaxed); + tel.published_frames.store(1, Ordering::Relaxed); + tel.presented_frames.store(1, Ordering::Relaxed); + tel.repeated_presents.store(3, Ordering::Relaxed); + tel.render_errors.store(4, Ordering::Relaxed); + tel.publish_errors.store(5, Ordering::Relaxed); + tel.present_errors.store(6, Ordering::Relaxed); + tel.stale_epoch_discards.store(7, Ordering::Relaxed); + tel.shutdown_timeouts.store(8, Ordering::Relaxed); + tel.worker_panics.store(9, Ordering::Relaxed); + tel.last_produced_frame_id.store(10, Ordering::Relaxed); + tel.last_consumed_frame_id.store(11, Ordering::Relaxed); + tel.last_published_frame_id.store(12, Ordering::Relaxed); + tel.last_presented_frame_id.store(13, Ordering::Relaxed); + tel.last_dropped_frame_id.store(14, Ordering::Relaxed); + tel.last_error_frame_id.store(15, Ordering::Relaxed); + tel.active_render_epoch.store(16, Ordering::Relaxed); + + let worker = tel.snapshot().render_worker_telemetry(); + + assert_eq!(worker.produced_submissions, 2); + assert_eq!(worker.replaced_before_consume, 1); + assert_eq!(worker.consumed_submissions, 1); + assert_eq!(worker.published_frames, 1); + assert_eq!(worker.presented_frames, 1); + assert_eq!(worker.repeated_frames, 3); + assert_eq!(worker.render_failures, 4); + assert_eq!(worker.publish_failures, 5); + assert_eq!(worker.present_failures, 6); + assert_eq!(worker.stale_submission_discards, 7); + assert_eq!(worker.shutdown_timeouts, 8); + assert_eq!(worker.worker_panics, 9); + assert_eq!(worker.last_produced_frame_id, 10); + assert_eq!(worker.last_consumed_frame_id, 11); + assert_eq!(worker.last_published_frame_id, 12); + assert_eq!(worker.last_presented_frame_id, 13); + assert_eq!(worker.last_dropped_frame_id, 14); + assert_eq!(worker.last_error_frame_id, 15); + assert_eq!(worker.active_render_epoch, 16); + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 91e7066c..3e5615b6 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md b/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md index d9b498fa..6d8df73d 100644 --- a/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md +++ b/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md @@ -2,7 +2,7 @@ id: PLN-0112 ticket: real-render-worker-establishment title: Define Render Worker Errors and Telemetry -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From 76d4e9c8cefb0b6619c1694eecbb1a7be69c9bef Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 16:09:39 +0100 Subject: [PATCH 38/47] implements PLN-0113 --- crates/console/prometeu-system/src/lib.rs | 4 +- .../src/services/vm_runtime/mod.rs | 2 + .../vm_runtime/render_worker_handoff.rs | 172 ++++++++++++++++++ discussion/index.ndjson | 2 +- ...plement-thread-safe-latest-wins-handoff.md | 2 +- 5 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 crates/console/prometeu-system/src/services/vm_runtime/render_worker_handoff.rs diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 4c9b08e7..0d2b441f 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -9,5 +9,7 @@ pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfi pub use services::fs; pub use services::process; pub use services::task; -pub use services::vm_runtime::VirtualMachineRuntime; +pub use services::vm_runtime::{ + RenderWorkerHandoff, RenderWorkerHandoffWait, VirtualMachineRuntime, +}; pub use services::windows; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index a522bb4f..ab562aaa 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -4,6 +4,7 @@ mod dispatch; mod frame_scheduler; mod lifecycle; pub mod render_manager; +pub mod render_worker_handoff; #[cfg(test)] mod tests; mod tick; @@ -17,6 +18,7 @@ use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier}; use prometeu_hal::{Gfx2dCommand, GfxUiCommand}; use prometeu_vm::VirtualMachine; pub use render_manager::{RenderManager, RenderRuntimeCapabilities}; +pub use render_worker_handoff::{RenderWorkerHandoff, RenderWorkerHandoffWait}; use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::AtomicU32; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_worker_handoff.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_worker_handoff.rs new file mode 100644 index 00000000..1630dbb3 --- /dev/null +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_worker_handoff.rs @@ -0,0 +1,172 @@ +use prometeu_hal::{RenderSubmission, RenderWorkerTelemetry}; +use std::sync::{Condvar, Mutex}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RenderWorkerHandoffWait { + Submission(RenderSubmission), + Shutdown, +} + +#[derive(Debug, Default)] +struct RenderWorkerHandoffState { + pending: Option, + shutdown_requested: bool, + telemetry: RenderWorkerTelemetry, +} + +#[derive(Debug, Default)] +pub struct RenderWorkerHandoff { + state: Mutex, + ready: Condvar, +} + +impl RenderWorkerHandoff { + pub fn publish(&self, submission: RenderSubmission) { + let mut state = self.state.lock().unwrap(); + if let Some(replaced) = state.pending.replace(submission) { + state.telemetry.record_replaced_before_consume(replaced.frame_id.get()); + } + let frame_id = state.pending.as_ref().expect("submission was just stored").frame_id; + state.telemetry.record_produced(frame_id.get()); + self.ready.notify_one(); + } + + pub fn take_latest(&self) -> Option { + let mut state = self.state.lock().unwrap(); + let submission = state.pending.take()?; + state.telemetry.record_consumed(submission.frame_id.get()); + Some(submission) + } + + pub fn wait_take(&self) -> RenderWorkerHandoffWait { + self.wait_take_with_hook(|| {}) + } + + pub fn request_shutdown(&self) { + let mut state = self.state.lock().unwrap(); + state.shutdown_requested = true; + self.ready.notify_all(); + } + + pub fn telemetry(&self) -> RenderWorkerTelemetry { + self.state.lock().unwrap().telemetry + } + + fn wait_take_with_hook(&self, mut before_wait: impl FnMut()) -> RenderWorkerHandoffWait { + let mut state = self.state.lock().unwrap(); + loop { + if let Some(submission) = state.pending.take() { + state.telemetry.record_consumed(submission.frame_id.get()); + return RenderWorkerHandoffWait::Submission(submission); + } + if state.shutdown_requested { + return RenderWorkerHandoffWait::Shutdown; + } + before_wait(); + state = self.ready.wait(state).unwrap(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use prometeu_hal::{FrameId, Game2DFramePacket}; + use std::sync::Arc; + use std::sync::mpsc; + use std::thread; + + fn submission(frame_id: u64) -> RenderSubmission { + RenderSubmission::game2d(FrameId::new(frame_id), Game2DFramePacket::default()) + } + + #[test] + fn render_worker_handoff_takes_owned_submission() { + let handoff = RenderWorkerHandoff::default(); + + assert!(handoff.take_latest().is_none()); + handoff.publish(submission(7)); + + let taken = handoff.take_latest().expect("pending submission"); + assert_eq!(taken.frame_id, FrameId::new(7)); + assert!(handoff.take_latest().is_none()); + assert_eq!(handoff.telemetry().produced_submissions, 1); + assert_eq!(handoff.telemetry().consumed_submissions, 1); + } + + #[test] + fn render_worker_handoff_wait_take_returns_pending_without_blocking() { + let handoff = RenderWorkerHandoff::default(); + handoff.publish(submission(8)); + + assert_eq!(handoff.wait_take(), RenderWorkerHandoffWait::Submission(submission(8))); + assert!(handoff.take_latest().is_none()); + } + + #[test] + fn render_worker_handoff_is_latest_wins_and_counts_replacements() { + let handoff = RenderWorkerHandoff::default(); + + handoff.publish(submission(1)); + handoff.publish(submission(2)); + handoff.publish(submission(3)); + + let taken = handoff.take_latest().expect("latest submission"); + let telemetry = handoff.telemetry(); + assert_eq!(taken.frame_id, FrameId::new(3)); + assert_eq!(telemetry.produced_submissions, 3); + assert_eq!(telemetry.replaced_before_consume, 2); + assert_eq!(telemetry.consumed_submissions, 1); + assert_eq!(telemetry.last_produced_frame_id, 3); + assert_eq!(telemetry.last_dropped_frame_id, 2); + } + + #[test] + fn render_worker_handoff_waits_for_publish_without_sleep() { + let handoff = Arc::new(RenderWorkerHandoff::default()); + let consumer_handoff = Arc::clone(&handoff); + let (waiting_tx, waiting_rx) = mpsc::channel(); + + let consumer = thread::spawn(move || { + let mut waiting_tx = Some(waiting_tx); + consumer_handoff.wait_take_with_hook(|| { + if let Some(tx) = waiting_tx.take() { + tx.send(()).unwrap(); + } + }) + }); + + waiting_rx.recv().expect("consumer reached condvar wait"); + handoff.publish(submission(42)); + + assert_eq!( + consumer.join().expect("consumer thread should finish"), + RenderWorkerHandoffWait::Submission(submission(42)) + ); + assert_eq!(handoff.telemetry().consumed_submissions, 1); + } + + #[test] + fn render_worker_handoff_wait_returns_shutdown_signal() { + let handoff = Arc::new(RenderWorkerHandoff::default()); + let consumer_handoff = Arc::clone(&handoff); + let (waiting_tx, waiting_rx) = mpsc::channel(); + + let consumer = thread::spawn(move || { + let mut waiting_tx = Some(waiting_tx); + consumer_handoff.wait_take_with_hook(|| { + if let Some(tx) = waiting_tx.take() { + tx.send(()).unwrap(); + } + }) + }); + + waiting_rx.recv().expect("consumer reached condvar wait"); + handoff.request_shutdown(); + + assert_eq!( + consumer.join().expect("consumer thread should finish"), + RenderWorkerHandoffWait::Shutdown + ); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 3e5615b6..7bb1ff99 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md b/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md index adffc62c..cb60d927 100644 --- a/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md +++ b/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md @@ -2,7 +2,7 @@ id: PLN-0113 ticket: real-render-worker-establishment title: Implement Thread-Safe Latest-Wins Handoff -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From 28105c2bf2805dc64717fe82d198bbfac3d25400 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 16:11:34 +0100 Subject: [PATCH 39/47] implements PLN-0114 --- .../src/services/vm_runtime/mod.rs | 2 + .../vm_runtime/render_worker_handoff.rs | 2 +- .../vm_runtime/render_worker_test_harness.rs | 166 ++++++++++++++++++ .../services/vm_runtime/tests_asset_bank.rs | 11 +- discussion/index.ndjson | 2 +- ...eterministic-render-worker-test-harness.md | 2 +- 6 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 crates/console/prometeu-system/src/services/vm_runtime/render_worker_test_harness.rs diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index ab562aaa..66709b9b 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -6,6 +6,8 @@ mod lifecycle; pub mod render_manager; pub mod render_worker_handoff; #[cfg(test)] +pub(crate) mod render_worker_test_harness; +#[cfg(test)] mod tests; mod tick; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_worker_handoff.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_worker_handoff.rs index 1630dbb3..5cae6c74 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_worker_handoff.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_worker_handoff.rs @@ -122,7 +122,7 @@ mod tests { } #[test] - fn render_worker_handoff_waits_for_publish_without_sleep() { + fn render_worker_handoff_waits_for_publish_without_timing_delay() { let handoff = Arc::new(RenderWorkerHandoff::default()); let consumer_handoff = Arc::clone(&handoff); let (waiting_tx, waiting_rx) = mpsc::channel(); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_worker_test_harness.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_worker_test_harness.rs new file mode 100644 index 00000000..205b7544 --- /dev/null +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_worker_test_harness.rs @@ -0,0 +1,166 @@ +use prometeu_hal::app_mode::AppMode; +use prometeu_hal::{ + FrameId, Game2DFramePacket, OwnedRgba8888Frame, RenderOwnership, RenderSubmission, + RenderWorkerError, ShellUiFramePacket, +}; +use std::sync::{Arc, Condvar, Mutex}; + +#[derive(Debug, Default)] +struct RenderGateState { + entered: u64, + released: u64, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct RenderGate { + state: Arc<(Mutex, Condvar)>, +} + +impl RenderGate { + pub(crate) fn block_until_released(&self) { + let (lock, condvar) = &*self.state; + let mut state = lock.lock().unwrap(); + state.entered = state.entered.wrapping_add(1); + let entry = state.entered; + condvar.notify_all(); + while state.released < entry { + state = condvar.wait(state).unwrap(); + } + } + + pub(crate) fn wait_for_entries(&self, count: u64) { + let (lock, condvar) = &*self.state; + let mut state = lock.lock().unwrap(); + while state.entered < count { + state = condvar.wait(state).unwrap(); + } + } + + pub(crate) fn release_one(&self) { + let (lock, condvar) = &*self.state; + let mut state = lock.lock().unwrap(); + state.released = state.released.wrapping_add(1); + condvar.notify_all(); + } +} + +#[derive(Debug, Default)] +pub(crate) struct FakeRenderBackend { + gate: Option, + next_error: Mutex>, + published_frames: Mutex>, +} + +impl FakeRenderBackend { + pub(crate) fn with_gate(gate: RenderGate) -> Self { + Self { gate: Some(gate), ..Default::default() } + } + + pub(crate) fn fail_next(&self, error: RenderWorkerError) { + *self.next_error.lock().unwrap() = Some(error); + } + + pub(crate) fn render( + &self, + submission: &RenderSubmission, + ) -> Result { + if let Some(gate) = &self.gate { + gate.block_until_released(); + } + + if let Some(error) = self.next_error.lock().unwrap().take() { + return Err(error); + } + + Ok(owned_frame(submission.frame_id, 0xff00_0000 | submission.frame_id.get() as u32)) + } + + pub(crate) fn publish(&self, frame: OwnedRgba8888Frame) { + self.published_frames.lock().unwrap().push(frame); + } + + pub(crate) fn published_frames(&self) -> Vec { + self.published_frames.lock().unwrap().clone() + } +} + +pub(crate) fn game_submission(frame_id: u64, epoch: u64, app_id: u32) -> RenderSubmission { + RenderSubmission::game2d(FrameId::new(frame_id), Game2DFramePacket::default()) + .with_ownership(RenderOwnership::new(epoch, AppMode::Game, app_id)) +} + +pub(crate) fn shell_submission(frame_id: u64, epoch: u64, app_id: u32) -> RenderSubmission { + RenderSubmission::shell_ui(FrameId::new(frame_id), ShellUiFramePacket::new(Vec::new())) + .with_ownership(RenderOwnership::new(epoch, AppMode::Shell, app_id)) +} + +pub(crate) fn owned_frame(frame_id: FrameId, pixel: u32) -> OwnedRgba8888Frame { + OwnedRgba8888Frame::packed( + frame_id, + RenderOwnership::new(0, AppMode::Game, 0), + 1, + 1, + vec![pixel], + ) + .expect("test frame should be valid") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::mpsc; + use std::thread; + + #[test] + fn render_worker_harness_blocks_and_releases_render_without_timing_delay() { + let gate = RenderGate::default(); + let backend = Arc::new(FakeRenderBackend::with_gate(gate.clone())); + let render_backend = Arc::clone(&backend); + let submission = game_submission(4, 1, 9); + let (done_tx, done_rx) = mpsc::channel(); + + let worker = thread::spawn(move || { + let frame = render_backend.render(&submission).expect("render should succeed"); + done_tx.send(frame.frame_id).unwrap(); + }); + + gate.wait_for_entries(1); + assert!(done_rx.try_recv().is_err()); + gate.release_one(); + + assert_eq!(done_rx.recv().expect("render should finish"), FrameId::new(4)); + worker.join().expect("worker thread should finish"); + } + + #[test] + fn render_worker_harness_records_published_owned_frames() { + let backend = FakeRenderBackend::default(); + let frame = owned_frame(FrameId::new(8), 0xff00_ff00); + + backend.publish(frame.clone()); + + assert_eq!(backend.published_frames(), vec![frame]); + } + + #[test] + fn render_worker_harness_injects_backend_errors() { + let backend = FakeRenderBackend::default(); + backend.fail_next(RenderWorkerError::RenderFailed); + + let error = backend.render(&game_submission(5, 1, 1)).expect_err("render should fail"); + + assert_eq!(error, RenderWorkerError::RenderFailed); + assert!(backend.render(&game_submission(6, 1, 1)).is_ok()); + } + + #[test] + fn render_worker_harness_builds_game_and_shell_submissions() { + let game = game_submission(1, 2, 3); + let shell = shell_submission(4, 5, 6); + + assert_eq!(game.frame_id, FrameId::new(1)); + assert_eq!(game.ownership, RenderOwnership::new(2, AppMode::Game, 3)); + assert_eq!(shell.frame_id, FrameId::new(4)); + assert_eq!(shell.ownership, RenderOwnership::new(5, AppMode::Shell, 6)); + } +} diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs index ccf91534..45ae5a2e 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs @@ -391,15 +391,20 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); - loop { + let mut asset_ready = false; + for _ in 0..1_000 { match platform.local_hardware().assets.status(handle) { - LoadStatus::READY => break, + LoadStatus::READY => { + asset_ready = true; + break; + } LoadStatus::PENDING | LoadStatus::LOADING => { - std::thread::sleep(std::time::Duration::from_millis(1)); + std::thread::yield_now(); } other => panic!("unexpected asset status before commit: {:?}", other), } } + assert!(asset_ready, "asset did not become ready before commit"); assert_eq!(platform.local_hardware().assets.commit(handle), AssetOpStatus::Ok); platform.local_hardware().assets.apply_commits(); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 7bb1ff99..a73aa74f 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md b/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md index 065b48f4..cba2fb1e 100644 --- a/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md +++ b/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md @@ -2,7 +2,7 @@ id: PLN-0114 ticket: real-render-worker-establishment title: Add Deterministic Render Worker Test Harness -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From 4b890850aa94532600793d161838407e7fac5d82 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 16:13:56 +0100 Subject: [PATCH 40/47] implements PLN-0115 --- crates/console/prometeu-system/src/lib.rs | 1 + .../src/services/vm_runtime/mod.rs | 4 + .../src/services/vm_runtime/render_worker.rs | 291 ++++++++++++++++++ discussion/index.ndjson | 2 +- ...ment-render-worker-controller-lifecycle.md | 2 +- 5 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 0d2b441f..d67c855d 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -10,6 +10,7 @@ pub use services::fs; pub use services::process; pub use services::task; pub use services::vm_runtime::{ + RenderWorkerBackend, RenderWorkerConfig, RenderWorkerController, RenderWorkerFrameSink, RenderWorkerHandoff, RenderWorkerHandoffWait, VirtualMachineRuntime, }; pub use services::windows; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index 66709b9b..2a5813a6 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -4,6 +4,7 @@ mod dispatch; mod frame_scheduler; mod lifecycle; pub mod render_manager; +mod render_worker; pub mod render_worker_handoff; #[cfg(test)] pub(crate) mod render_worker_test_harness; @@ -20,6 +21,9 @@ use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier}; use prometeu_hal::{Gfx2dCommand, GfxUiCommand}; use prometeu_vm::VirtualMachine; pub use render_manager::{RenderManager, RenderRuntimeCapabilities}; +pub use render_worker::{ + RenderWorkerBackend, RenderWorkerConfig, RenderWorkerController, RenderWorkerFrameSink, +}; pub use render_worker_handoff::{RenderWorkerHandoff, RenderWorkerHandoffWait}; use std::collections::HashMap; use std::sync::Arc; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs new file mode 100644 index 00000000..b89a8919 --- /dev/null +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs @@ -0,0 +1,291 @@ +use crate::services::vm_runtime::{RenderWorkerHandoff, RenderWorkerHandoffWait}; +use prometeu_hal::{ + FrameId, OwnedRgba8888Frame, RenderSubmission, RenderWorkerError, RenderWorkerTelemetry, +}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::mpsc::{self, Receiver, RecvTimeoutError}; +use std::sync::{Arc, Mutex}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +pub trait RenderWorkerBackend: Send + 'static { + fn render( + &self, + submission: &RenderSubmission, + ) -> Result; +} + +pub trait RenderWorkerFrameSink: Send + 'static { + fn publish(&self, frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError>; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RenderWorkerConfig { + pub shutdown_timeout: Duration, +} + +impl Default for RenderWorkerConfig { + fn default() -> Self { + Self { shutdown_timeout: Duration::from_millis(250) } + } +} + +#[derive(Debug, Default)] +struct RenderWorkerState { + telemetry: RenderWorkerTelemetry, + last_error: Option, +} + +#[derive(Debug)] +pub struct RenderWorkerController { + config: RenderWorkerConfig, + handoff: Arc, + state: Arc>, + handle: Option>, + done_rx: Receiver>, +} + +impl RenderWorkerController { + pub fn start( + config: RenderWorkerConfig, + handoff: Arc, + backend: B, + sink: S, + ) -> Self + where + B: RenderWorkerBackend, + S: RenderWorkerFrameSink, + { + let state = Arc::new(Mutex::new(RenderWorkerState::default())); + let worker_handoff = Arc::clone(&handoff); + let worker_state = Arc::clone(&state); + let panic_state = Arc::clone(&state); + let (done_tx, done_rx) = mpsc::channel(); + + let handle = thread::spawn(move || { + let result = catch_unwind(AssertUnwindSafe(|| { + run_worker_loop(worker_handoff, worker_state, backend, sink); + })) + .map_err(|_| RenderWorkerError::WorkerPanic); + if let Err(error) = result { + record_error(&panic_state, error, FrameId::ZERO); + } + let _ = done_tx.send(result); + }); + + Self { config, handoff, state, handle: Some(handle), done_rx } + } + + pub fn stop(&mut self) -> Result<(), RenderWorkerError> { + self.handoff.request_shutdown(); + match self.done_rx.recv_timeout(self.config.shutdown_timeout) { + Ok(result) => { + if let Some(handle) = self.handle.take() { + if handle.join().is_err() { + record_error(&self.state, RenderWorkerError::WorkerPanic, FrameId::ZERO); + return Err(RenderWorkerError::WorkerPanic); + } + } + result + } + Err(RecvTimeoutError::Timeout) => { + record_error(&self.state, RenderWorkerError::ShutdownTimeout, FrameId::ZERO); + Err(RenderWorkerError::ShutdownTimeout) + } + Err(RecvTimeoutError::Disconnected) => { + record_error(&self.state, RenderWorkerError::InternalFailure, FrameId::ZERO); + Err(RenderWorkerError::InternalFailure) + } + } + } + + pub fn telemetry(&self) -> RenderWorkerTelemetry { + let mut telemetry = self.handoff.telemetry(); + let state = self.state.lock().unwrap(); + telemetry.published_frames = state.telemetry.published_frames; + telemetry.render_failures = state.telemetry.render_failures; + telemetry.publish_failures = state.telemetry.publish_failures; + telemetry.present_failures = state.telemetry.present_failures; + telemetry.shutdown_timeouts = state.telemetry.shutdown_timeouts; + telemetry.worker_panics = state.telemetry.worker_panics; + telemetry.last_published_frame_id = state.telemetry.last_published_frame_id; + telemetry.last_error_frame_id = state.telemetry.last_error_frame_id; + telemetry + } + + pub fn last_error(&self) -> Option { + self.state.lock().unwrap().last_error + } +} + +fn run_worker_loop( + handoff: Arc, + state: Arc>, + backend: B, + sink: S, +) where + B: RenderWorkerBackend, + S: RenderWorkerFrameSink, +{ + loop { + let submission = match handoff.wait_take() { + RenderWorkerHandoffWait::Submission(submission) => submission, + RenderWorkerHandoffWait::Shutdown => return, + }; + let frame_id = submission.frame_id; + + let render_result = catch_unwind(AssertUnwindSafe(|| backend.render(&submission))) + .map_err(|_| RenderWorkerError::WorkerPanic); + let frame = match render_result { + Ok(Ok(frame)) => frame, + Ok(Err(error)) | Err(error) => { + record_error(&state, error, frame_id); + continue; + } + }; + + let publish_result = catch_unwind(AssertUnwindSafe(|| sink.publish(frame))) + .map_err(|_| RenderWorkerError::WorkerPanic); + match publish_result { + Ok(Ok(())) => record_published(&state, frame_id), + Ok(Err(error)) | Err(error) => record_error(&state, error, frame_id), + } + } +} + +fn record_published(state: &Arc>, frame_id: FrameId) { + state.lock().unwrap().telemetry.record_published(frame_id.get()); +} + +fn record_error( + state: &Arc>, + error: RenderWorkerError, + frame_id: FrameId, +) { + let mut state = state.lock().unwrap(); + state.last_error = Some(error); + state.telemetry.record_error(error, frame_id.get()); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::services::vm_runtime::render_worker_test_harness::{ + FakeRenderBackend, RenderGate, game_submission, + }; + use prometeu_hal::{FrameId, OwnedRgba8888Frame}; + + impl RenderWorkerBackend for Arc { + fn render( + &self, + submission: &RenderSubmission, + ) -> Result { + FakeRenderBackend::render(self, submission) + } + } + + impl RenderWorkerFrameSink for Arc { + fn publish(&self, frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError> { + FakeRenderBackend::publish(self, frame); + Ok(()) + } + } + + #[derive(Debug, Default)] + struct PanickingBackend; + + impl RenderWorkerBackend for PanickingBackend { + fn render( + &self, + _submission: &RenderSubmission, + ) -> Result { + panic!("backend panic") + } + } + + #[derive(Debug, Default)] + struct NoopSink; + + impl RenderWorkerFrameSink for NoopSink { + fn publish(&self, _frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError> { + Ok(()) + } + } + + #[test] + fn render_worker_controller_starts_and_stops_waiting_worker() { + let handoff = Arc::new(RenderWorkerHandoff::default()); + let backend = Arc::new(FakeRenderBackend::default()); + let mut controller = RenderWorkerController::start( + RenderWorkerConfig::default(), + Arc::clone(&handoff), + Arc::clone(&backend), + backend, + ); + + assert_eq!(controller.stop(), Ok(())); + assert_eq!(controller.last_error(), None); + } + + #[test] + fn render_worker_controller_renders_and_publishes_submission() { + let handoff = Arc::new(RenderWorkerHandoff::default()); + let backend = Arc::new(FakeRenderBackend::default()); + let mut controller = RenderWorkerController::start( + RenderWorkerConfig::default(), + Arc::clone(&handoff), + Arc::clone(&backend), + Arc::clone(&backend), + ); + + handoff.publish(game_submission(11, 1, 1)); + assert_eq!(controller.stop(), Ok(())); + + let frames = backend.published_frames(); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0].frame_id, FrameId::new(11)); + assert_eq!(controller.telemetry().published_frames, 1); + } + + #[test] + fn render_worker_controller_reports_shutdown_timeout() { + let handoff = Arc::new(RenderWorkerHandoff::default()); + let gate = RenderGate::default(); + let backend = Arc::new(FakeRenderBackend::with_gate(gate.clone())); + let mut controller = RenderWorkerController::start( + RenderWorkerConfig { shutdown_timeout: Duration::from_millis(1) }, + Arc::clone(&handoff), + Arc::clone(&backend), + Arc::clone(&backend), + ); + + handoff.publish(game_submission(12, 1, 1)); + gate.wait_for_entries(1); + + assert_eq!(controller.stop(), Err(RenderWorkerError::ShutdownTimeout)); + assert_eq!(controller.last_error(), Some(RenderWorkerError::ShutdownTimeout)); + assert_eq!(controller.telemetry().shutdown_timeouts, 1); + + gate.release_one(); + controller.config.shutdown_timeout = Duration::from_millis(250); + assert_eq!(controller.stop(), Ok(())); + } + + #[test] + fn render_worker_controller_captures_backend_panic_as_typed_failure() { + let handoff = Arc::new(RenderWorkerHandoff::default()); + let mut controller = RenderWorkerController::start( + RenderWorkerConfig::default(), + Arc::clone(&handoff), + PanickingBackend, + NoopSink, + ); + + handoff.publish(game_submission(13, 1, 1)); + assert_eq!(controller.stop(), Ok(())); + + assert_eq!(controller.last_error(), Some(RenderWorkerError::WorkerPanic)); + assert_eq!(controller.telemetry().worker_panics, 1); + assert_eq!(controller.telemetry().last_error_frame_id, 13); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index a73aa74f..cbce166c 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md b/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md index 509fe667..97bb37b3 100644 --- a/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md +++ b/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md @@ -2,7 +2,7 @@ id: PLN-0115 ticket: real-render-worker-establishment title: Implement Render Worker Controller Lifecycle -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From 0c6a669e6e32f0b6eb68e80bf047f66f1e59c936 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 16:17:16 +0100 Subject: [PATCH 41/47] implements PLN-0116 --- crates/console/prometeu-drivers/src/lib.rs | 2 + .../src/local_render_backend.rs | 111 ++++++++++++++++++ crates/console/prometeu-hal/src/lib.rs | 4 +- .../console/prometeu-hal/src/render_worker.rs | 13 ++ crates/console/prometeu-system/src/lib.rs | 5 +- .../src/services/vm_runtime/mod.rs | 4 +- .../src/services/vm_runtime/render_worker.rs | 38 +++--- discussion/index.ndjson | 2 +- ...116-implement-fake-local-render-backend.md | 2 +- 9 files changed, 150 insertions(+), 31 deletions(-) create mode 100644 crates/console/prometeu-drivers/src/local_render_backend.rs diff --git a/crates/console/prometeu-drivers/src/lib.rs b/crates/console/prometeu-drivers/src/lib.rs index c8fe4afd..675eeb0e 100644 --- a/crates/console/prometeu-drivers/src/lib.rs +++ b/crates/console/prometeu-drivers/src/lib.rs @@ -3,6 +3,7 @@ mod audio; mod frame_composer; mod gfx; pub mod hardware; +mod local_render_backend; mod memory_banks; mod pad; mod test_platform; @@ -12,6 +13,7 @@ pub use crate::asset::AssetManager; pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; pub use crate::frame_composer::{ComposerBuffer, FrameComposer, SceneStatus, SpriteController}; pub use crate::gfx::Gfx; +pub use crate::local_render_backend::LocalFramebufferRenderBackend; pub use crate::memory_banks::{ GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess, SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller, diff --git a/crates/console/prometeu-drivers/src/local_render_backend.rs b/crates/console/prometeu-drivers/src/local_render_backend.rs new file mode 100644 index 00000000..87bbe446 --- /dev/null +++ b/crates/console/prometeu-drivers/src/local_render_backend.rs @@ -0,0 +1,111 @@ +use crate::gfx::Gfx; +use crate::memory_banks::GlyphBankPoolAccess; +use prometeu_hal::app_mode::AppMode; +use prometeu_hal::{ + OwnedRgba8888Frame, RenderSubmission, RenderSubmissionPacket, RenderWorkerBackend, + RenderWorkerError, +}; +use std::sync::{Arc, Mutex}; + +pub struct LocalFramebufferRenderBackend { + gfx: Mutex, +} + +impl LocalFramebufferRenderBackend { + pub fn new(width: usize, height: usize, glyph_banks: Arc) -> Self { + Self { gfx: Mutex::new(Gfx::new(width, height, glyph_banks)) } + } + + pub fn render_submission( + &self, + submission: &RenderSubmission, + ) -> Result { + let mut gfx = self.gfx.lock().map_err(|_| RenderWorkerError::InternalFailure)?; + match &submission.packet { + RenderSubmissionPacket::Game2D(packet) if submission.app_mode == AppMode::Game => { + gfx.render_game2d_frame_packet(packet); + } + RenderSubmissionPacket::ShellUi(packet) if submission.app_mode == AppMode::Shell => { + gfx.render_shell_ui_frame_packet(packet); + } + _ => return Err(RenderWorkerError::RenderFailed), + } + + gfx.present(); + let (width, height) = gfx.size(); + OwnedRgba8888Frame::packed( + submission.frame_id, + submission.ownership, + width, + height, + gfx.front_buffer().to_vec(), + ) + .map_err(|_| RenderWorkerError::InternalFailure) + } +} + +impl RenderWorkerBackend for LocalFramebufferRenderBackend { + fn render( + &self, + submission: &RenderSubmission, + ) -> Result { + self.render_submission(submission) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::memory_banks::MemoryBanks; + use prometeu_hal::app_mode::AppMode; + use prometeu_hal::color::Color; + use prometeu_hal::primitives::Rect; + use prometeu_hal::{ + FrameId, Game2DFramePacket, Gfx2dCommand, GfxUiCommand, RenderOwnership, ShellUiFramePacket, + }; + + fn backend() -> LocalFramebufferRenderBackend { + let banks = Arc::new(MemoryBanks::new()); + LocalFramebufferRenderBackend::new(4, 4, banks) + } + + #[test] + fn local_backend_renders_game2d_submission_to_owned_frame() { + let backend = backend(); + let mut packet = Game2DFramePacket::default(); + packet.gfx2d = vec![Gfx2dCommand::FillRect { + rect: Rect { x: 1, y: 1, w: 2, h: 2 }, + color: Color::RED, + }]; + let ownership = RenderOwnership::new(3, AppMode::Game, 9); + let submission = + RenderSubmission::game2d(FrameId::new(5), packet).with_ownership(ownership); + + let frame = backend.render(&submission).expect("game frame should render"); + + assert_eq!(frame.frame_id, FrameId::new(5)); + assert_eq!(frame.ownership, ownership); + assert_eq!(frame.width, 4); + assert_eq!(frame.height, 4); + assert_eq!(frame.pixels[1 + 4], Color::RED.raw()); + assert_eq!(frame.pixels[2 + 2 * 4], Color::RED.raw()); + } + + #[test] + fn local_backend_renders_shell_ui_submission_to_owned_frame() { + let backend = backend(); + let packet = ShellUiFramePacket::new(vec![GfxUiCommand::FillRect { + rect: Rect { x: 0, y: 0, w: 1, h: 1 }, + color: Color::GREEN, + }]); + let ownership = RenderOwnership::new(4, AppMode::Shell, 10); + let submission = + RenderSubmission::shell_ui(FrameId::new(6), packet).with_ownership(ownership); + + let frame = backend.render(&submission).expect("shell frame should render"); + + assert_eq!(frame.frame_id, FrameId::new(6)); + assert_eq!(frame.ownership, ownership); + assert_eq!(frame.pixels[0], Color::GREEN.raw()); + } +} diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index 62c6863d..106bfc05 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -60,5 +60,7 @@ pub use render_submission::{ Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderOwnership, RenderSubmission, RenderSubmissionPacket, ShellUiFramePacket, }; -pub use render_worker::{RenderWorkerError, RenderWorkerTelemetry}; +pub use render_worker::{ + RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, RenderWorkerTelemetry, +}; pub use touch_bridge::TouchBridge; diff --git a/crates/console/prometeu-hal/src/render_worker.rs b/crates/console/prometeu-hal/src/render_worker.rs index 44ebbc60..91eae5a4 100644 --- a/crates/console/prometeu-hal/src/render_worker.rs +++ b/crates/console/prometeu-hal/src/render_worker.rs @@ -1,5 +1,7 @@ use std::fmt; +use crate::{OwnedRgba8888Frame, RenderSubmission}; + /// Typed failure surface for the real render worker and its backend. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RenderWorkerError { @@ -30,6 +32,17 @@ impl fmt::Display for RenderWorkerError { impl std::error::Error for RenderWorkerError {} +pub trait RenderWorkerBackend: Send + 'static { + fn render( + &self, + submission: &RenderSubmission, + ) -> Result; +} + +pub trait RenderWorkerFrameSink: Send + 'static { + fn publish(&self, frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError>; +} + /// Snapshot of render worker handoff, render, publish, and shutdown counters. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct RenderWorkerTelemetry { diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index d67c855d..8c74e484 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -6,11 +6,12 @@ mod services; pub use crash_report::CrashReport; pub use os::{LifecycleError, LifecycleOperation, SystemOS}; pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate}; +pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink}; pub use services::fs; pub use services::process; pub use services::task; pub use services::vm_runtime::{ - RenderWorkerBackend, RenderWorkerConfig, RenderWorkerController, RenderWorkerFrameSink, - RenderWorkerHandoff, RenderWorkerHandoffWait, VirtualMachineRuntime, + RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff, RenderWorkerHandoffWait, + VirtualMachineRuntime, }; pub use services::windows; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index 2a5813a6..d3319452 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -21,9 +21,7 @@ use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier}; use prometeu_hal::{Gfx2dCommand, GfxUiCommand}; use prometeu_vm::VirtualMachine; pub use render_manager::{RenderManager, RenderRuntimeCapabilities}; -pub use render_worker::{ - RenderWorkerBackend, RenderWorkerConfig, RenderWorkerController, RenderWorkerFrameSink, -}; +pub use render_worker::{RenderWorkerConfig, RenderWorkerController}; pub use render_worker_handoff::{RenderWorkerHandoff, RenderWorkerHandoffWait}; use std::collections::HashMap; use std::sync::Arc; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs index b89a8919..1792e790 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs @@ -1,6 +1,6 @@ use crate::services::vm_runtime::{RenderWorkerHandoff, RenderWorkerHandoffWait}; use prometeu_hal::{ - FrameId, OwnedRgba8888Frame, RenderSubmission, RenderWorkerError, RenderWorkerTelemetry, + FrameId, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, RenderWorkerTelemetry, }; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::mpsc::{self, Receiver, RecvTimeoutError}; @@ -8,17 +8,6 @@ use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use std::time::Duration; -pub trait RenderWorkerBackend: Send + 'static { - fn render( - &self, - submission: &RenderSubmission, - ) -> Result; -} - -pub trait RenderWorkerFrameSink: Send + 'static { - fn publish(&self, frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError>; -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RenderWorkerConfig { pub shutdown_timeout: Duration, @@ -173,20 +162,23 @@ mod tests { use crate::services::vm_runtime::render_worker_test_harness::{ FakeRenderBackend, RenderGate, game_submission, }; - use prometeu_hal::{FrameId, OwnedRgba8888Frame}; + use prometeu_hal::{FrameId, OwnedRgba8888Frame, RenderSubmission}; - impl RenderWorkerBackend for Arc { + #[derive(Clone, Debug)] + struct SharedFakeBackend(Arc); + + impl RenderWorkerBackend for SharedFakeBackend { fn render( &self, submission: &RenderSubmission, ) -> Result { - FakeRenderBackend::render(self, submission) + self.0.render(submission) } } - impl RenderWorkerFrameSink for Arc { + impl RenderWorkerFrameSink for SharedFakeBackend { fn publish(&self, frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError> { - FakeRenderBackend::publish(self, frame); + self.0.publish(frame); Ok(()) } } @@ -219,8 +211,8 @@ mod tests { let mut controller = RenderWorkerController::start( RenderWorkerConfig::default(), Arc::clone(&handoff), - Arc::clone(&backend), - backend, + SharedFakeBackend(Arc::clone(&backend)), + SharedFakeBackend(backend), ); assert_eq!(controller.stop(), Ok(())); @@ -234,8 +226,8 @@ mod tests { let mut controller = RenderWorkerController::start( RenderWorkerConfig::default(), Arc::clone(&handoff), - Arc::clone(&backend), - Arc::clone(&backend), + SharedFakeBackend(Arc::clone(&backend)), + SharedFakeBackend(Arc::clone(&backend)), ); handoff.publish(game_submission(11, 1, 1)); @@ -255,8 +247,8 @@ mod tests { let mut controller = RenderWorkerController::start( RenderWorkerConfig { shutdown_timeout: Duration::from_millis(1) }, Arc::clone(&handoff), - Arc::clone(&backend), - Arc::clone(&backend), + SharedFakeBackend(Arc::clone(&backend)), + SharedFakeBackend(Arc::clone(&backend)), ); handoff.publish(game_submission(12, 1, 1)); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index cbce166c..138b7cf0 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md b/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md index f1c94e45..4aacc4bf 100644 --- a/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md +++ b/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md @@ -2,7 +2,7 @@ id: PLN-0116 ticket: real-render-worker-establishment title: Implement Fake Local Render Backend -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From cfb5b1ac89d2dc2661aa71eb6e7e0421fd371930 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 15 Jun 2026 16:20:46 +0100 Subject: [PATCH 42/47] implements PLN-0117 --- .../vm_runtime/async_render_contract_tests.rs | 6 +- .../src/services/vm_runtime/lifecycle.rs | 37 ++++++++++++ .../src/services/vm_runtime/mod.rs | 2 + .../src/services/vm_runtime/render_manager.rs | 14 ++++- .../src/services/vm_runtime/render_worker.rs | 59 ++++++++++++++++++- .../src/services/vm_runtime/tick.rs | 19 ++++-- discussion/index.ndjson | 2 +- ...ntegrate-runtime-submission-with-worker.md | 2 +- 8 files changed, 126 insertions(+), 15 deletions(-) diff --git a/crates/console/prometeu-system/src/services/vm_runtime/async_render_contract_tests.rs b/crates/console/prometeu-system/src/services/vm_runtime/async_render_contract_tests.rs index bc145990..613e694c 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/async_render_contract_tests.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/async_render_contract_tests.rs @@ -112,7 +112,7 @@ fn matrix_packet_policy_and_pacing_are_mode_specific() { assert_eq!(game_policy.execution, RenderExecutionPolicy::WorkerCapableWithLocalFallback); assert_eq!( game_policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }), - RenderConsumerPath::LocalWorkerPrototype + RenderConsumerPath::RealWorker ); let shell_policy = RenderPolicy::for_app_mode(AppMode::Shell); @@ -163,7 +163,7 @@ fn matrix_sync_and_worker_paths_share_latest_wins_handoff_contract() { let outcome = match path { RenderConsumerPath::LocalSynchronous => manager.consume_latest(&mut surface), - RenderConsumerPath::LocalWorkerPrototype => { + RenderConsumerPath::RealWorker => { let mut worker = LocalRenderWorker; worker.consume_latest(&mut manager, &mut surface) } @@ -172,7 +172,7 @@ fn matrix_sync_and_worker_paths_share_latest_wins_handoff_contract() { (outcome, surface.seen, manager.render_telemetry().replaced_before_consume) } - for path in [RenderConsumerPath::LocalSynchronous, RenderConsumerPath::LocalWorkerPrototype] { + for path in [RenderConsumerPath::LocalSynchronous, RenderConsumerPath::RealWorker] { let (outcome, seen, replaced) = consume_with(path); assert_eq!(outcome, RenderConsumeOutcome::Presented { frame_id: FrameId::new(1) }); assert_eq!(seen, vec![FrameId::new(1)]); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs index 71d26ba5..09478c82 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs @@ -3,6 +3,7 @@ use crate::CrashReport; use crate::fs::{FsBackend, FsState, VirtualFS}; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::log::{LogLevel, LogSource}; +use prometeu_hal::{RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink}; use std::collections::HashMap; impl VirtualMachineRuntime { @@ -25,6 +26,8 @@ impl VirtualMachineRuntime { frame_scheduler: FrameScheduler::default(), render_capabilities: RenderRuntimeCapabilities::default(), render_manager: RenderManager::new(AppMode::Game), + render_worker_handoff: Arc::new(RenderWorkerHandoff::default()), + render_worker_controller: None, gfx2d_commands: Vec::new(), gfxui_commands: Vec::new(), logs_written_this_frame: HashMap::new(), @@ -124,6 +127,8 @@ impl VirtualMachineRuntime { self.frame_scheduler = FrameScheduler::default(); self.render_capabilities = RenderRuntimeCapabilities::default(); self.render_manager = RenderManager::new(AppMode::Game); + let _ = self.stop_render_worker(); + self.render_worker_handoff = Arc::new(RenderWorkerHandoff::default()); self.gfx2d_commands.clear(); self.gfxui_commands.clear(); self.logs_written_this_frame.clear(); @@ -147,6 +152,38 @@ impl VirtualMachineRuntime { self.render_capabilities = capabilities; } + pub fn start_render_worker(&mut self, config: RenderWorkerConfig, backend: B, sink: S) + where + B: RenderWorkerBackend, + S: RenderWorkerFrameSink, + { + let _ = self.stop_render_worker(); + self.render_worker_handoff = Arc::new(RenderWorkerHandoff::default()); + self.render_worker_controller = Some(RenderWorkerController::start( + config, + Arc::clone(&self.render_worker_handoff), + backend, + sink, + )); + self.render_capabilities.game_render_worker = true; + } + + pub fn stop_render_worker(&mut self) -> Result<(), RenderWorkerError> { + if let Some(mut controller) = self.render_worker_controller.take() { + controller.stop() + } else { + Ok(()) + } + } + + pub(crate) fn publish_pending_render_to_worker(&mut self) -> bool { + let Some(submission) = self.render_manager.take_pending_for_worker() else { + return false; + }; + self.render_worker_handoff.publish(submission); + true + } + pub fn initialize_vm( &mut self, log_service: &mut LogService, diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index d3319452..d0f69ebe 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -41,6 +41,8 @@ pub struct VirtualMachineRuntime { pub frame_scheduler: FrameScheduler, pub render_capabilities: RenderRuntimeCapabilities, pub render_manager: RenderManager, + pub render_worker_handoff: Arc, + pub render_worker_controller: Option, pub gfx2d_commands: Vec, pub gfxui_commands: Vec, pub logs_written_this_frame: HashMap, diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs index 613acc41..17666af2 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs @@ -59,7 +59,7 @@ pub struct RenderRuntimeCapabilities { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RenderConsumerPath { LocalSynchronous, - LocalWorkerPrototype, + RealWorker, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -95,7 +95,7 @@ impl RenderPolicy { ) -> RenderConsumerPath { match (self.execution, capabilities.game_render_worker) { (RenderExecutionPolicy::WorkerCapableWithLocalFallback, true) => { - RenderConsumerPath::LocalWorkerPrototype + RenderConsumerPath::RealWorker } _ => RenderConsumerPath::LocalSynchronous, } @@ -107,8 +107,10 @@ pub trait RenderSurface { } #[derive(Debug, Default)] +#[cfg(test)] pub struct LocalRenderWorker; +#[cfg(test)] impl LocalRenderWorker { pub fn consume_latest( &mut self, @@ -329,6 +331,12 @@ impl RenderManager { discarded } + pub fn take_pending_for_worker(&mut self) -> Option { + let submission = self.handoff.take_latest()?; + self.telemetry.record_consumed(submission.frame_id); + Some(submission) + } + pub fn request_shutdown(&mut self) -> Option { self.consumer_state = RenderConsumerState::Stopped; self.discard_pending_submission() @@ -624,7 +632,7 @@ mod tests { ); assert_eq!( policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }), - RenderConsumerPath::LocalWorkerPrototype + RenderConsumerPath::RealWorker ); } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs index 1792e790..1464f477 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs @@ -107,6 +107,14 @@ impl RenderWorkerController { } } +impl Drop for RenderWorkerController { + fn drop(&mut self) { + if self.handle.is_some() { + let _ = self.stop(); + } + } +} + fn run_worker_loop( handoff: Arc, state: Arc>, @@ -159,10 +167,11 @@ fn record_error( #[cfg(test)] mod tests { use super::*; + use crate::services::vm_runtime::VirtualMachineRuntime; use crate::services::vm_runtime::render_worker_test_harness::{ FakeRenderBackend, RenderGate, game_submission, }; - use prometeu_hal::{FrameId, OwnedRgba8888Frame, RenderSubmission}; + use prometeu_hal::{FrameId, Game2DFramePacket, OwnedRgba8888Frame, RenderSubmission}; #[derive(Clone, Debug)] struct SharedFakeBackend(Arc); @@ -280,4 +289,52 @@ mod tests { assert_eq!(controller.telemetry().worker_panics, 1); assert_eq!(controller.telemetry().last_error_frame_id, 13); } + + #[test] + fn runtime_publishes_game_submissions_to_worker_without_waiting_for_render() { + let gate = RenderGate::default(); + let backend = Arc::new(FakeRenderBackend::with_gate(gate.clone())); + let mut runtime = VirtualMachineRuntime::new(None); + runtime.start_render_worker( + RenderWorkerConfig::default(), + SharedFakeBackend(Arc::clone(&backend)), + SharedFakeBackend(Arc::clone(&backend)), + ); + + runtime + .render_manager + .close_frame_with_packet(prometeu_hal::RenderSubmissionPacket::Game2D( + Game2DFramePacket::default(), + )) + .expect("game packet should close"); + assert!(runtime.publish_pending_render_to_worker()); + gate.wait_for_entries(1); + + runtime + .render_manager + .close_frame_with_packet(prometeu_hal::RenderSubmissionPacket::Game2D( + Game2DFramePacket::default(), + )) + .expect("second game packet should close"); + assert!(runtime.publish_pending_render_to_worker()); + runtime + .render_manager + .close_frame_with_packet(prometeu_hal::RenderSubmissionPacket::Game2D( + Game2DFramePacket::default(), + )) + .expect("third game packet should close"); + assert!(runtime.publish_pending_render_to_worker()); + + assert_eq!(runtime.render_worker_handoff.telemetry().replaced_before_consume, 1); + + gate.release_one(); + gate.wait_for_entries(2); + gate.release_one(); + + assert_eq!(runtime.stop_render_worker(), Ok(())); + let frames = backend.published_frames(); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].frame_id, FrameId::ZERO); + assert_eq!(frames[1].frame_id, FrameId::new(2)); + } } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index ec12b75a..a2dad5e0 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -1,7 +1,5 @@ use super::dispatch::VmRuntimeHost; -use super::render_manager::{ - LocalRenderWorker, RenderConsumeOutcome, RenderConsumerPath, RenderSurface, -}; +use super::render_manager::{RenderConsumeOutcome, RenderConsumerPath, RenderSurface}; use super::*; use crate::CrashReport; use crate::fs::{FsState, VirtualFS}; @@ -273,9 +271,18 @@ impl VirtualMachineRuntime { RenderConsumerPath::LocalSynchronous => { self.render_manager.consume_latest(&mut surface) } - RenderConsumerPath::LocalWorkerPrototype => { - let mut worker = LocalRenderWorker; - worker.consume_latest(&mut self.render_manager, &mut surface) + RenderConsumerPath::RealWorker + if self.render_worker_controller.is_some() + && self.current_cartridge_app_mode == AppMode::Game => + { + if self.publish_pending_render_to_worker() { + RenderConsumeOutcome::NoSubmission + } else { + RenderConsumeOutcome::NoSubmission + } + } + RenderConsumerPath::RealWorker => { + self.render_manager.consume_latest(&mut surface) } } })); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 138b7cf0..8b340908 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md b/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md index 9fa5f8ea..15509c7a 100644 --- a/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md +++ b/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md @@ -2,7 +2,7 @@ id: PLN-0117 ticket: real-render-worker-establishment title: Integrate Runtime Submission With Worker -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From 6b5b27d068cbe9aa574a6a5ebb9c93ce9738567f Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 16 Jun 2026 05:38:02 +0100 Subject: [PATCH 43/47] implements PLN-0118 --- crates/console/prometeu-system/src/lib.rs | 2 +- .../src/services/vm_runtime/lifecycle.rs | 9 ++ .../src/services/vm_runtime/mod.rs | 3 +- .../src/services/vm_runtime/render_worker.rs | 132 ++++++++++++++++-- .../vm_runtime/render_worker_test_harness.rs | 9 +- .../src/services/vm_runtime/tick.rs | 1 + discussion/index.ndjson | 2 +- ...d-stale-epoch-and-repeat-frame-behavior.md | 2 +- 8 files changed, 147 insertions(+), 13 deletions(-) diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 8c74e484..8822e7ea 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -12,6 +12,6 @@ pub use services::process; pub use services::task; pub use services::vm_runtime::{ RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff, RenderWorkerHandoffWait, - VirtualMachineRuntime, + RenderWorkerOwnership, VirtualMachineRuntime, }; pub use services::windows; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs index 09478c82..68c58069 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs @@ -27,6 +27,7 @@ impl VirtualMachineRuntime { render_capabilities: RenderRuntimeCapabilities::default(), render_manager: RenderManager::new(AppMode::Game), render_worker_handoff: Arc::new(RenderWorkerHandoff::default()), + render_worker_ownership: RenderWorkerOwnership::default(), render_worker_controller: None, gfx2d_commands: Vec::new(), gfxui_commands: Vec::new(), @@ -129,6 +130,8 @@ impl VirtualMachineRuntime { self.render_manager = RenderManager::new(AppMode::Game); let _ = self.stop_render_worker(); self.render_worker_handoff = Arc::new(RenderWorkerHandoff::default()); + self.render_worker_ownership = + RenderWorkerOwnership::new(self.render_manager.active_ownership()); self.gfx2d_commands.clear(); self.gfxui_commands.clear(); self.logs_written_this_frame.clear(); @@ -159,9 +162,11 @@ impl VirtualMachineRuntime { { let _ = self.stop_render_worker(); self.render_worker_handoff = Arc::new(RenderWorkerHandoff::default()); + self.render_worker_ownership.set(self.render_manager.active_ownership()); self.render_worker_controller = Some(RenderWorkerController::start( config, Arc::clone(&self.render_worker_handoff), + self.render_worker_ownership.clone(), backend, sink, )); @@ -184,6 +189,10 @@ impl VirtualMachineRuntime { true } + pub(crate) fn sync_render_worker_ownership(&self) { + self.render_worker_ownership.set(self.render_manager.active_ownership()); + } + pub fn initialize_vm( &mut self, log_service: &mut LogService, diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index d0f69ebe..5312ec3f 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -21,7 +21,7 @@ use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier}; use prometeu_hal::{Gfx2dCommand, GfxUiCommand}; use prometeu_vm::VirtualMachine; pub use render_manager::{RenderManager, RenderRuntimeCapabilities}; -pub use render_worker::{RenderWorkerConfig, RenderWorkerController}; +pub use render_worker::{RenderWorkerConfig, RenderWorkerController, RenderWorkerOwnership}; pub use render_worker_handoff::{RenderWorkerHandoff, RenderWorkerHandoffWait}; use std::collections::HashMap; use std::sync::Arc; @@ -42,6 +42,7 @@ pub struct VirtualMachineRuntime { pub render_capabilities: RenderRuntimeCapabilities, pub render_manager: RenderManager, pub render_worker_handoff: Arc, + pub render_worker_ownership: RenderWorkerOwnership, pub render_worker_controller: Option, pub gfx2d_commands: Vec, pub gfxui_commands: Vec, diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs index 1464f477..7993ec8f 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs @@ -1,6 +1,8 @@ use crate::services::vm_runtime::{RenderWorkerHandoff, RenderWorkerHandoffWait}; +use prometeu_hal::app_mode::AppMode; use prometeu_hal::{ - FrameId, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, RenderWorkerTelemetry, + FrameId, OwnedRgba8888Frame, RenderOwnership, RenderWorkerBackend, RenderWorkerError, + RenderWorkerFrameSink, RenderWorkerTelemetry, }; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::mpsc::{self, Receiver, RecvTimeoutError}; @@ -23,12 +25,39 @@ impl Default for RenderWorkerConfig { struct RenderWorkerState { telemetry: RenderWorkerTelemetry, last_error: Option, + latest_published_frame: Option, +} + +#[derive(Clone, Debug)] +pub struct RenderWorkerOwnership { + active: Arc>, +} + +impl Default for RenderWorkerOwnership { + fn default() -> Self { + Self::new(RenderOwnership::new(0, AppMode::Game, 0)) + } +} + +impl RenderWorkerOwnership { + pub fn new(active: RenderOwnership) -> Self { + Self { active: Arc::new(Mutex::new(active)) } + } + + pub fn snapshot(&self) -> RenderOwnership { + *self.active.lock().unwrap() + } + + pub fn set(&self, active: RenderOwnership) { + *self.active.lock().unwrap() = active; + } } #[derive(Debug)] pub struct RenderWorkerController { config: RenderWorkerConfig, handoff: Arc, + ownership: RenderWorkerOwnership, state: Arc>, handle: Option>, done_rx: Receiver>, @@ -38,6 +67,7 @@ impl RenderWorkerController { pub fn start( config: RenderWorkerConfig, handoff: Arc, + ownership: RenderWorkerOwnership, backend: B, sink: S, ) -> Self @@ -47,13 +77,14 @@ impl RenderWorkerController { { let state = Arc::new(Mutex::new(RenderWorkerState::default())); let worker_handoff = Arc::clone(&handoff); + let worker_ownership = ownership.clone(); let worker_state = Arc::clone(&state); let panic_state = Arc::clone(&state); let (done_tx, done_rx) = mpsc::channel(); let handle = thread::spawn(move || { let result = catch_unwind(AssertUnwindSafe(|| { - run_worker_loop(worker_handoff, worker_state, backend, sink); + run_worker_loop(worker_handoff, worker_ownership, worker_state, backend, sink); })) .map_err(|_| RenderWorkerError::WorkerPanic); if let Err(error) = result { @@ -62,7 +93,7 @@ impl RenderWorkerController { let _ = done_tx.send(result); }); - Self { config, handoff, state, handle: Some(handle), done_rx } + Self { config, handoff, ownership, state, handle: Some(handle), done_rx } } pub fn stop(&mut self) -> Result<(), RenderWorkerError> { @@ -95,8 +126,12 @@ impl RenderWorkerController { telemetry.render_failures = state.telemetry.render_failures; telemetry.publish_failures = state.telemetry.publish_failures; telemetry.present_failures = state.telemetry.present_failures; + telemetry.stale_submission_discards = state.telemetry.stale_submission_discards; + telemetry.repeated_frames = state.telemetry.repeated_frames; telemetry.shutdown_timeouts = state.telemetry.shutdown_timeouts; telemetry.worker_panics = state.telemetry.worker_panics; + telemetry.last_presented_frame_id = state.telemetry.last_presented_frame_id; + telemetry.last_dropped_frame_id = state.telemetry.last_dropped_frame_id; telemetry.last_published_frame_id = state.telemetry.last_published_frame_id; telemetry.last_error_frame_id = state.telemetry.last_error_frame_id; telemetry @@ -105,6 +140,21 @@ impl RenderWorkerController { pub fn last_error(&self) -> Option { self.state.lock().unwrap().last_error } + + pub fn active_ownership(&self) -> RenderOwnership { + self.ownership.snapshot() + } + + pub fn latest_published_frame(&self) -> Option { + self.state.lock().unwrap().latest_published_frame.clone() + } + + pub fn repeat_latest_frame(&self) -> Option { + let mut state = self.state.lock().unwrap(); + let frame = state.latest_published_frame.clone()?; + state.telemetry.record_repeated_frame(frame.frame_id.get()); + Some(frame) + } } impl Drop for RenderWorkerController { @@ -117,6 +167,7 @@ impl Drop for RenderWorkerController { fn run_worker_loop( handoff: Arc, + ownership: RenderWorkerOwnership, state: Arc>, backend: B, sink: S, @@ -141,17 +192,28 @@ fn run_worker_loop( } }; - let publish_result = catch_unwind(AssertUnwindSafe(|| sink.publish(frame))) + if frame.ownership != ownership.snapshot() { + record_stale_discard(&state, frame_id); + continue; + } + + let publish_result = catch_unwind(AssertUnwindSafe(|| sink.publish(frame.clone()))) .map_err(|_| RenderWorkerError::WorkerPanic); match publish_result { - Ok(Ok(())) => record_published(&state, frame_id), + Ok(Ok(())) => record_published(&state, frame), Ok(Err(error)) | Err(error) => record_error(&state, error, frame_id), } } } -fn record_published(state: &Arc>, frame_id: FrameId) { - state.lock().unwrap().telemetry.record_published(frame_id.get()); +fn record_published(state: &Arc>, frame: OwnedRgba8888Frame) { + let mut state = state.lock().unwrap(); + state.telemetry.record_published(frame.frame_id.get()); + state.latest_published_frame = Some(frame); +} + +fn record_stale_discard(state: &Arc>, frame_id: FrameId) { + state.lock().unwrap().telemetry.record_stale_submission_discarded(frame_id.get()); } fn record_error( @@ -171,7 +233,10 @@ mod tests { use crate::services::vm_runtime::render_worker_test_harness::{ FakeRenderBackend, RenderGate, game_submission, }; - use prometeu_hal::{FrameId, Game2DFramePacket, OwnedRgba8888Frame, RenderSubmission}; + use prometeu_hal::app_mode::AppMode; + use prometeu_hal::{ + FrameId, Game2DFramePacket, OwnedRgba8888Frame, RenderOwnership, RenderSubmission, + }; #[derive(Clone, Debug)] struct SharedFakeBackend(Arc); @@ -220,6 +285,7 @@ mod tests { let mut controller = RenderWorkerController::start( RenderWorkerConfig::default(), Arc::clone(&handoff), + RenderWorkerOwnership::default(), SharedFakeBackend(Arc::clone(&backend)), SharedFakeBackend(backend), ); @@ -235,6 +301,7 @@ mod tests { let mut controller = RenderWorkerController::start( RenderWorkerConfig::default(), Arc::clone(&handoff), + RenderWorkerOwnership::new(RenderOwnership::new(1, AppMode::Game, 1)), SharedFakeBackend(Arc::clone(&backend)), SharedFakeBackend(Arc::clone(&backend)), ); @@ -256,6 +323,7 @@ mod tests { let mut controller = RenderWorkerController::start( RenderWorkerConfig { shutdown_timeout: Duration::from_millis(1) }, Arc::clone(&handoff), + RenderWorkerOwnership::default(), SharedFakeBackend(Arc::clone(&backend)), SharedFakeBackend(Arc::clone(&backend)), ); @@ -278,6 +346,7 @@ mod tests { let mut controller = RenderWorkerController::start( RenderWorkerConfig::default(), Arc::clone(&handoff), + RenderWorkerOwnership::default(), PanickingBackend, NoopSink, ); @@ -290,6 +359,53 @@ mod tests { assert_eq!(controller.telemetry().last_error_frame_id, 13); } + #[test] + fn render_worker_controller_discards_stale_frame_before_publish() { + let handoff = Arc::new(RenderWorkerHandoff::default()); + let gate = RenderGate::default(); + let backend = Arc::new(FakeRenderBackend::with_gate(gate.clone())); + let ownership = RenderWorkerOwnership::new(RenderOwnership::new(1, AppMode::Game, 1)); + let mut controller = RenderWorkerController::start( + RenderWorkerConfig::default(), + Arc::clone(&handoff), + ownership.clone(), + SharedFakeBackend(Arc::clone(&backend)), + SharedFakeBackend(Arc::clone(&backend)), + ); + + handoff.publish(game_submission(21, 1, 1)); + gate.wait_for_entries(1); + ownership.set(RenderOwnership::new(2, AppMode::Game, 1)); + gate.release_one(); + + assert_eq!(controller.stop(), Ok(())); + assert!(backend.published_frames().is_empty()); + assert_eq!(controller.telemetry().stale_submission_discards, 1); + } + + #[test] + fn render_worker_controller_repeats_latest_published_frame_without_rendering() { + let handoff = Arc::new(RenderWorkerHandoff::default()); + let backend = Arc::new(FakeRenderBackend::default()); + let mut controller = RenderWorkerController::start( + RenderWorkerConfig::default(), + Arc::clone(&handoff), + RenderWorkerOwnership::new(RenderOwnership::new(1, AppMode::Game, 1)), + SharedFakeBackend(Arc::clone(&backend)), + SharedFakeBackend(Arc::clone(&backend)), + ); + + handoff.publish(game_submission(22, 1, 1)); + assert_eq!(controller.stop(), Ok(())); + + let latest = controller.latest_published_frame().expect("latest frame"); + let repeated = controller.repeat_latest_frame().expect("repeated frame"); + + assert_eq!(latest, repeated); + assert_eq!(backend.published_frames().len(), 1); + assert_eq!(controller.telemetry().repeated_frames, 1); + } + #[test] fn runtime_publishes_game_submissions_to_worker_without_waiting_for_render() { let gate = RenderGate::default(); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_worker_test_harness.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_worker_test_harness.rs index 205b7544..2a6511f6 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_worker_test_harness.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_worker_test_harness.rs @@ -72,7 +72,14 @@ impl FakeRenderBackend { return Err(error); } - Ok(owned_frame(submission.frame_id, 0xff00_0000 | submission.frame_id.get() as u32)) + OwnedRgba8888Frame::packed( + submission.frame_id, + submission.ownership, + 1, + 1, + vec![0xff00_0000 | submission.frame_id.get() as u32], + ) + .map_err(|_| RenderWorkerError::InternalFailure) } pub(crate) fn publish(&self, frame: OwnedRgba8888Frame) { diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index a2dad5e0..78da52bd 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -246,6 +246,7 @@ impl VirtualMachineRuntime { self.current_cartridge_app_mode, self.current_app_id, ); + self.sync_render_worker_ownership(); if self.current_cartridge_app_mode == AppMode::Game { let mut packet = platform.game2d_frame_composer().close_game2d_packet(); packet.gfx2d = std::mem::take(&mut self.gfx2d_commands); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 8b340908..5e175e79 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-15","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-16","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md b/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md index 343ac32d..59301fa6 100644 --- a/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md +++ b/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md @@ -2,7 +2,7 @@ id: PLN-0118 ticket: real-render-worker-establishment title: Add Stale Epoch and Repeat Frame Behavior -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From 67a629dbe4da0d069a65955b537cbecb5db21adb Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 16 Jun 2026 05:44:08 +0100 Subject: [PATCH 44/47] implements PLN-0119 --- .../console/prometeu-drivers/src/hardware.rs | 3 + .../src/local_render_backend.rs | 57 ++++++++++++++++++- .../prometeu-system/src/os/facades/vm.rs | 30 +++++++++- .../src/services/vm_runtime/lifecycle.rs | 12 +++- .../prometeu-host-desktop-winit/src/runner.rs | 55 +++++++++++++++--- .../src/utilities.rs | 27 ++++++++- discussion/index.ndjson | 2 +- ...egrate-desktop-host-worker-presentation.md | 2 +- 8 files changed, 171 insertions(+), 17 deletions(-) diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 6646e461..cd4d3cf9 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -33,6 +33,8 @@ use std::sync::Arc; /// - **Audio**: Stereo, Command-based mixing. /// - **Input**: 12-button Digital Gamepad + Absolute Touch/Mouse. pub struct Hardware { + /// Shared physical memory bank ownership used by peripherals and worker backends. + pub memory_banks: Arc, /// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps. pub gfx: Gfx, /// Canonical frame orchestration owner for scene/camera/cache/resolver/sprites. @@ -216,6 +218,7 @@ impl Hardware { ); let glyph_slot_index = assets.glyph_asset_slot_index(); Self { + memory_banks: Arc::clone(&memory_banks), gfx: Gfx::new( Self::W, Self::H, diff --git a/crates/console/prometeu-drivers/src/local_render_backend.rs b/crates/console/prometeu-drivers/src/local_render_backend.rs index 87bbe446..2bed802c 100644 --- a/crates/console/prometeu-drivers/src/local_render_backend.rs +++ b/crates/console/prometeu-drivers/src/local_render_backend.rs @@ -1,5 +1,7 @@ +use crate::asset::GlyphAssetSlotIndex; +use crate::frame_composer::FrameComposer; use crate::gfx::Gfx; -use crate::memory_banks::GlyphBankPoolAccess; +use crate::memory_banks::{GlyphBankPoolAccess, MemoryBanks, SceneBankPoolAccess}; use prometeu_hal::app_mode::AppMode; use prometeu_hal::{ OwnedRgba8888Frame, RenderSubmission, RenderSubmissionPacket, RenderWorkerBackend, @@ -9,11 +11,51 @@ use std::sync::{Arc, Mutex}; pub struct LocalFramebufferRenderBackend { gfx: Mutex, + frame_composer: Mutex, } impl LocalFramebufferRenderBackend { pub fn new(width: usize, height: usize, glyph_banks: Arc) -> Self { - Self { gfx: Mutex::new(Gfx::new(width, height, glyph_banks)) } + Self::new_with_parts( + width, + height, + glyph_banks, + Arc::new(MemoryBanks::new()), + GlyphAssetSlotIndex::new(), + ) + } + + pub fn new_with_memory_banks( + width: usize, + height: usize, + memory_banks: Arc, + glyph_slot_index: GlyphAssetSlotIndex, + ) -> Self { + Self::new_with_parts( + width, + height, + Arc::clone(&memory_banks) as Arc, + memory_banks, + glyph_slot_index, + ) + } + + fn new_with_parts( + width: usize, + height: usize, + glyph_banks: Arc, + scene_banks: Arc, + glyph_slot_index: GlyphAssetSlotIndex, + ) -> Self { + Self { + gfx: Mutex::new(Gfx::new(width, height, glyph_banks)), + frame_composer: Mutex::new(FrameComposer::new( + width, + height, + scene_banks, + glyph_slot_index, + )), + } } pub fn render_submission( @@ -23,7 +65,16 @@ impl LocalFramebufferRenderBackend { let mut gfx = self.gfx.lock().map_err(|_| RenderWorkerError::InternalFailure)?; match &submission.packet { RenderSubmissionPacket::Game2D(packet) if submission.app_mode == AppMode::Game => { - gfx.render_game2d_frame_packet(packet); + if packet.composer.bound_scene.is_none() { + gfx.render_game2d_frame_packet(packet); + } else { + let mut frame_composer = self + .frame_composer + .lock() + .map_err(|_| RenderWorkerError::InternalFailure)?; + frame_composer.render_game2d_frame_packet(packet, &mut *gfx); + gfx.apply_gfx2d_commands(&packet.gfx2d); + } } RenderSubmissionPacket::ShellUi(packet) if submission.app_mode == AppMode::Shell => { gfx.render_shell_ui_frame_packet(packet); diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index f2d3f065..20cecd80 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -1,9 +1,13 @@ use crate::CrashReport; use crate::os::SystemOS; +use crate::{RenderWorkerConfig, RenderWorkerController}; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; -use prometeu_hal::{InputSignals, RuntimePlatform}; +use prometeu_hal::{ + InputSignals, OwnedRgba8888Frame, RenderWorkerBackend, RenderWorkerError, + RenderWorkerFrameSink, RuntimePlatform, +}; use prometeu_vm::VirtualMachine; use std::sync::atomic::Ordering; @@ -96,6 +100,30 @@ impl<'a> VmFacade<'a> { self.os.vm_runtime.atomic_telemetry.snapshot() } + pub fn start_render_worker(&mut self, config: RenderWorkerConfig, backend: B, sink: S) + where + B: RenderWorkerBackend, + S: RenderWorkerFrameSink, + { + self.os.vm_runtime.start_render_worker(config, backend, sink); + } + + pub fn stop_render_worker(&mut self) -> Result<(), RenderWorkerError> { + self.os.vm_runtime.stop_render_worker() + } + + pub fn render_worker_controller(&self) -> Option<&RenderWorkerController> { + self.os.vm_runtime.render_worker_controller.as_ref() + } + + pub fn latest_render_worker_frame(&self) -> Option { + self.os.vm_runtime.latest_render_worker_frame() + } + + pub fn repeat_latest_render_worker_frame(&self) -> Option { + self.os.vm_runtime.repeat_latest_render_worker_frame() + } + pub fn cert_config(&self) -> &CertificationConfig { &self.os.vm_runtime.certifier.config } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs index 68c58069..7463e0c1 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs @@ -3,7 +3,9 @@ use crate::CrashReport; use crate::fs::{FsBackend, FsState, VirtualFS}; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::log::{LogLevel, LogSource}; -use prometeu_hal::{RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink}; +use prometeu_hal::{ + OwnedRgba8888Frame, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, +}; use std::collections::HashMap; impl VirtualMachineRuntime { @@ -193,6 +195,14 @@ impl VirtualMachineRuntime { self.render_worker_ownership.set(self.render_manager.active_ownership()); } + pub fn latest_render_worker_frame(&self) -> Option { + self.render_worker_controller.as_ref()?.latest_published_frame() + } + + pub fn repeat_latest_render_worker_frame(&self) -> Option { + self.render_worker_controller.as_ref()?.repeat_latest_frame() + } + pub fn initialize_vm( &mut self, log_service: &mut LogService, diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index 53235b3d..9354b54b 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -4,14 +4,16 @@ use crate::fs_backend::HostDirBackend; use crate::input::HostInputHandler; use crate::log_sink::HostConsoleSink; use crate::stats::HostStats; -use crate::utilities::draw_rgba8888_to_rgba8; +use crate::utilities::{draw_owned_rgba8888_frame_to_rgba8, draw_rgba8888_to_rgba8}; use pixels::wgpu::PresentMode; use pixels::{Pixels, PixelsBuilder, SurfaceTexture}; -use prometeu_drivers::AudioCommand; use prometeu_drivers::hardware::Hardware; +use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks}; use prometeu_firmware::{BootTarget, Firmware, FirmwareState}; -use prometeu_hal::RuntimePlatform; use prometeu_hal::telemetry::CertificationConfig; +use prometeu_hal::{OwnedRgba8888Frame, RenderWorkerError, RenderWorkerFrameSink, RuntimePlatform}; +use prometeu_system::RenderWorkerConfig; +use std::sync::Arc; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; use winit::dpi::LogicalSize; @@ -22,6 +24,15 @@ use winit::window::{Window, WindowAttributes, WindowId}; const IDLE_HOST_POLL_DT: Duration = Duration::from_millis(100); +#[derive(Debug, Clone, Copy)] +struct HostWorkerFrameSink; + +impl RenderWorkerFrameSink for HostWorkerFrameSink { + fn publish(&self, _frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError> { + Ok(()) + } +} + #[derive(Debug, Clone)] struct PresentationState { latest_published_frame: u64, @@ -153,8 +164,20 @@ impl HostRunner { firmware.os.fs().mount(Box::new(backend)); } - let hardware = Hardware::new(); + let memory_banks = Arc::new(MemoryBanks::new()); + let hardware = Hardware::new_with_memory_banks(Arc::clone(&memory_banks)); let display_size = hardware.display_size(); + let worker_backend = LocalFramebufferRenderBackend::new_with_memory_banks( + display_size.0, + display_size.1, + memory_banks, + hardware.assets.glyph_asset_slot_index(), + ); + firmware.os.vm().start_render_worker( + RenderWorkerConfig::default(), + worker_backend, + HostWorkerFrameSink, + ); Self { window: None, @@ -278,10 +301,17 @@ impl ApplicationHandler for HostRunner { // Mutable borrow of the frame (lasts only within this block) let frame = pixels.frame_mut(); - // Immutable borrow of prometeu-core (different field, ok) - let src = self.hardware.gfx.front_buffer(); - - draw_rgba8888_to_rgba8(src, frame); + let use_worker_frame = + matches!(self.firmware.state, FirmwareState::GameRunning(_)); + if use_worker_frame + && let Some(worker_frame) = + self.firmware.os.vm().repeat_latest_render_worker_frame() + { + draw_owned_rgba8888_frame_to_rgba8(&worker_frame, frame); + } else { + let src = self.hardware.gfx.front_buffer(); + draw_rgba8888_to_rgba8(src, frame); + } } // <- frame borrow ends here if pixels.render().is_err() { @@ -372,7 +402,14 @@ impl ApplicationHandler for HostRunner { self.stats.record_frame(); } - self.presentation.note_published_frame(self.firmware.os.frame_index()); + let use_worker_frame = matches!(self.firmware.state, FirmwareState::GameRunning(_)); + if use_worker_frame + && let Some(worker_frame) = self.firmware.os.vm().latest_render_worker_frame() + { + self.presentation.note_published_frame(worker_frame.frame_id.get()); + } else { + self.presentation.note_published_frame(self.firmware.os.frame_index()); + } if was_debugger_connected != self.debugger.stream.is_some() || was_waiting_for_start != self.debugger.waiting_for_start diff --git a/crates/host/prometeu-host-desktop-winit/src/utilities.rs b/crates/host/prometeu-host-desktop-winit/src/utilities.rs index 30eb7933..fe6c9ad1 100644 --- a/crates/host/prometeu-host-desktop-winit/src/utilities.rs +++ b/crates/host/prometeu-host-desktop-winit/src/utilities.rs @@ -1,3 +1,5 @@ +use prometeu_hal::OwnedRgba8888Frame; + /// Copies RGBA8888 pixels in canonical RGBA raw order into the `pixels` RGBA8 frame. pub fn draw_rgba8888_to_rgba8(src: &[u32], dst_rgba: &mut [u8]) { for (i, &px) in src.iter().enumerate() { @@ -9,9 +11,15 @@ pub fn draw_rgba8888_to_rgba8(src: &[u32], dst_rgba: &mut [u8]) { } } +pub fn draw_owned_rgba8888_frame_to_rgba8(frame: &OwnedRgba8888Frame, dst_rgba: &mut [u8]) { + draw_rgba8888_to_rgba8(&frame.pixels[..frame.required_pixel_len()], dst_rgba); +} + #[cfg(test)] mod tests { - use super::draw_rgba8888_to_rgba8; + use super::{draw_owned_rgba8888_frame_to_rgba8, draw_rgba8888_to_rgba8}; + use prometeu_hal::app_mode::AppMode; + use prometeu_hal::{FrameId, OwnedRgba8888Frame, RenderOwnership}; #[test] fn draw_rgba8888_to_rgba8_preserves_channel_order_and_alpha() { @@ -22,4 +30,21 @@ mod tests { assert_eq!(dst, [0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x01]); } + + #[test] + fn draw_owned_frame_to_rgba8_uses_owned_rgba8888_pixels() { + let frame = OwnedRgba8888Frame::packed( + FrameId::new(3), + RenderOwnership::new(1, AppMode::Game, 7), + 1, + 1, + vec![0x10203040], + ) + .expect("test frame should be valid"); + let mut dst = [0; 4]; + + draw_owned_rgba8888_frame_to_rgba8(&frame, &mut dst); + + assert_eq!(dst, [0x10, 0x20, 0x30, 0x40]); + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 5e175e79..29ccbc53 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-16","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-16","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md b/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md index 2c97edd8..843e2f09 100644 --- a/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md +++ b/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md @@ -2,7 +2,7 @@ id: PLN-0119 ticket: real-render-worker-establishment title: Integrate Desktop Host Worker Presentation -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] From 850ca8691b7cf9b5b4a48b139988dd7490b2b18f Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 16 Jun 2026 05:46:13 +0100 Subject: [PATCH 45/47] implements PLN-0120 --- discussion/index.ndjson | 2 +- ...LN-0120-update-real-render-worker-specs.md | 2 +- docs/specs/runtime/04-gfx-peripheral.md | 44 +++++++++++++++++++ .../runtime/09-events-and-concurrency.md | 31 ++++++++++++- ...ortability-and-cross-platform-execution.md | 11 ++++- docs/specs/runtime/README.md | 7 +++ 6 files changed, 92 insertions(+), 5 deletions(-) diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 29ccbc53..d41e1fad 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-16","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-16","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md b/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md index b87f770e..2392d491 100644 --- a/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md +++ b/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md @@ -2,7 +2,7 @@ id: PLN-0120 ticket: real-render-worker-establishment title: Update Real Render Worker Specs -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] diff --git a/docs/specs/runtime/04-gfx-peripheral.md b/docs/specs/runtime/04-gfx-peripheral.md index 1ee1e924..05680535 100644 --- a/docs/specs/runtime/04-gfx-peripheral.md +++ b/docs/specs/runtime/04-gfx-peripheral.md @@ -73,6 +73,24 @@ Color values in the runtime, HAL, host-facing framebuffer, and GFX ABI are logical RGBA8888 values. RGB565 is not a supported runtime framebuffer, palette, host presentation, or compatibility contract. +### Published owned frame + +The canonical worker-published frame value is `OwnedRgba8888Frame`. + +`OwnedRgba8888Frame` contains: + +- the logical `FrameId`; +- render ownership metadata; +- width and height in pixels; +- stride in pixels; +- an owned `Vec` containing packed RGBA8888 pixels in canonical RGBA + channel order. + +The pixel vector is owned by the published frame. Consumers may copy it into a +host upload buffer, retain it as the latest published frame, or repeat it for a +host redraw. Consumers MUST NOT interpret the frame as a native texture, +swapchain image, window handle, or host GPU resource. + --- ## 3. Double Buffering @@ -134,6 +152,11 @@ to the logical/runtime side before handoff, or to an owning service. They MUST NOT require the render consumer to hold mutable VM, `Hardware`, `Gfx`, or `FrameComposer` references. +Read-only resource APIs expose compact lookup by ID. They do not expose mutable +bank state, `Arc` ownership as part of the contract, copies of whole banks, or +snapshot payloads. The implementation may choose its internal sharing mechanism, +but the render boundary contract is ID-based read-only access. + Local host implementations may keep a concrete hardware object internally as a platform implementation detail. That object is not part of the runtime-facing render boundary; the boundary is the typed platform service set. @@ -155,6 +178,12 @@ latest-wins semantics: The consumer status is telemetry. It is not a semantic ACK to the VM. +The real render worker consumes owned `RenderSubmission` values from this +single-slot handoff and publishes owned `OwnedRgba8888Frame` values. The +producer path MUST remain non-blocking with respect to worker rasterization and +host present. A slow worker can cause replacement/drop telemetry, but it MUST +NOT create an unbounded queue or stall VM logical frame production. + ### 4.4 Frame pacing Game logical frames are paced by the runtime frame scheduler, not by render @@ -170,6 +199,10 @@ sequential. The render consumer may repeat the last valid frame, and telemetry records the overrun/repeat. The VM MUST NOT produce catch-up frames to skip from frame `N` to frame `N+k`. +Repeating a frame means reusing the latest published `OwnedRgba8888Frame` +without recomposing, rerunning VM code, or consuming a new submission. Repeat +behavior is presentation cadence behavior, not guest-visible execution. + ### 4.5 AppMode policy Render execution policy is explicit by pipeline/AppMode: @@ -231,6 +264,17 @@ Minimum render telemetry includes: - last produced, consumed, presented, dropped, and error frame IDs; - active render epoch. +### 4.8 Shutdown and typed failures + +Render worker shutdown is bounded and observable. A shutdown request MUST +wake a waiting worker, discard pending submissions that will not be consumed, +and either join within the configured timeout or report a typed shutdown +failure. + +Worker backend failures, sink publication failures, stale ownership discards, +panic capture, and shutdown timeout are typed render worker outcomes. They are +recorded through telemetry and diagnostics; they do not become VM-visible ACKs. + --- ## 5. PROMETEU Graphical Structure diff --git a/docs/specs/runtime/09-events-and-concurrency.md b/docs/specs/runtime/09-events-and-concurrency.md index 200ad704..9337ad90 100644 --- a/docs/specs/runtime/09-events-and-concurrency.md +++ b/docs/specs/runtime/09-events-and-concurrency.md @@ -100,7 +100,34 @@ Important properties: - no execution occurs outside the frame loop; - frame structure remains observable for host tooling and host-owned certification. -## 7 Determinism and Best Practices +## 7 Render Worker Concurrency + +The render worker is not a machine-visible event source and does not introduce +guest callbacks. It is an implementation-side consumer of closed render +submissions. + +The render worker handoff uses single-slot latest-wins semantics: + +- a producer publishes at most one pending owned `RenderSubmission`; +- a newer submission replaces an older unconsumed pending submission; +- replacement is counted as telemetry; +- the producer does not wait for worker rasterization or host present; +- the worker takes ownership of the submission it consumes. + +The worker publishes `OwnedRgba8888Frame` values. Each published frame owns its +RGBA8888 pixel vector and carries frame and ownership metadata. Repeating a +frame reuses the latest published owned frame and does not execute guest code +or recompose the frame. + +Render resources reachable from a submission are resolved through read-only +ID-based access. The worker MUST NOT hold mutable VM state, mutable `Hardware`, +mutable `Gfx`, or a live mutable `FrameComposer` as its cross-thread contract. + +Shutdown is explicit and bounded. A shutdown request wakes a waiting worker, +causes pending work that will not be consumed to be discarded, and reports a +typed failure if the worker cannot join within the configured timeout. + +## 8 Determinism and Best Practices PROMETEU encourages: @@ -115,7 +142,7 @@ PROMETEU discourages: - hidden timing channels; - ambiguous out-of-band execution. -## 8 Relationship to Other Specs +## 9 Relationship to Other Specs - [`09a-coroutines-and-cooperative-scheduling.md`](09a-coroutines-and-cooperative-scheduling.md) defines coroutine lifecycle and scheduling behavior. - [`10-debug-inspection-and-profiling.md`](10-debug-inspection-and-profiling.md) defines observability and diagnostics surfaces. diff --git a/docs/specs/runtime/11-portability-and-cross-platform-execution.md b/docs/specs/runtime/11-portability-and-cross-platform-execution.md index 845e74d8..82c96016 100644 --- a/docs/specs/runtime/11-portability-and-cross-platform-execution.md +++ b/docs/specs/runtime/11-portability-and-cross-platform-execution.md @@ -118,6 +118,8 @@ Hardware differences: The graphics system: - produces typed logical render submissions that render to RGBA8888 output +- may consume Game submissions through a render worker that publishes owned + `OwnedRgba8888Frame` values - uses an indexed palette - does not depend on a specific GPU @@ -126,7 +128,8 @@ The platform layer: - exposes typed service facades for render submission, render backend execution, Game 2D composition, input, audio, assets, and telemetry - consumes closed render submissions through a render-surface implementation -- transports published RGBA8888 output into a host presentation surface without +- transports published RGBA8888 output or worker-published + `OwnedRgba8888Frame` pixels into a host presentation surface without injecting host-owned debug overlay pixels - is the runtime-facing portability boundary; runtime and firmware code must not depend on a monolithic hardware bridge or on a concrete hardware aggregate @@ -135,6 +138,12 @@ The host presentation layer MUST treat RGBA8888 as the canonical logical framebuffer format. RGB565 conversion is not part of the normal host presentation contract. +Native upload and present belong to the host event loop. The render worker MUST +NOT own or require a native window, swapchain, `pixels` surface, SDL texture, or +host GPU texture as part of its contract. The worker output is an owned +RGBA8888 frame; the host decides how to upload that frame to the native +presentation API for the current platform. + Host presentation SHOULD be driven by published render submissions and explicit host-owned invalidation, not by perpetual redraw polling. diff --git a/docs/specs/runtime/README.md b/docs/specs/runtime/README.md index a2d3ae67..f8ee55ef 100644 --- a/docs/specs/runtime/README.md +++ b/docs/specs/runtime/README.md @@ -56,6 +56,13 @@ input, audio, assets, and telemetry. A concrete local hardware aggregate may exist inside a host or test platform, but it is not the normative runtime-facing contract. +The real render worker contract is documented across the GFX, events, and +portability chapters. `04-gfx-peripheral.md` defines the render submission and +`OwnedRgba8888Frame` publication contract, `09-events-and-concurrency.md` +defines latest-wins handoff and bounded shutdown behavior, and +`11-portability-and-cross-platform-execution.md` defines the split between +worker-owned RGBA8888 frame production and host-owned native upload/present. + ## Document Functions - `normative`: defines the technical contract, expected behavior, or implementation-facing surface. From aa917cf44ec3e50af4b7a104097670de1d0bd1ec Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 16 Jun 2026 05:49:46 +0100 Subject: [PATCH 46/47] implements PLN-0121 --- .../prometeu-host-desktop-winit/src/runner.rs | 26 +++++++++++++++---- discussion/index.ndjson | 2 +- ...al-worker-path-validation-and-hardening.md | 11 +++++++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index 9354b54b..5c4871f3 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -97,6 +97,10 @@ fn desired_control_flow( } } +fn should_present_worker_frame(state: &FirmwareState) -> bool { + matches!(state, FirmwareState::GameRunning(_)) +} + /// The Desktop implementation of the PROMETEU Runtime. /// /// This struct acts as the physical "chassis" of the virtual console. It is @@ -301,9 +305,7 @@ impl ApplicationHandler for HostRunner { // Mutable borrow of the frame (lasts only within this block) let frame = pixels.frame_mut(); - let use_worker_frame = - matches!(self.firmware.state, FirmwareState::GameRunning(_)); - if use_worker_frame + if should_present_worker_frame(&self.firmware.state) && let Some(worker_frame) = self.firmware.os.vm().repeat_latest_render_worker_frame() { @@ -402,8 +404,7 @@ impl ApplicationHandler for HostRunner { self.stats.record_frame(); } - let use_worker_frame = matches!(self.firmware.state, FirmwareState::GameRunning(_)); - if use_worker_frame + if should_present_worker_frame(&self.firmware.state) && let Some(worker_frame) = self.firmware.os.vm().latest_render_worker_frame() { self.presentation.note_published_frame(worker_frame.frame_id.get()); @@ -449,8 +450,12 @@ impl ApplicationHandler for HostRunner { mod tests { use super::*; use prometeu_firmware::BootTarget; + use prometeu_firmware::firmware::firmware_state::{ + GameRunningStep, HubHomeStep, ShellRunningStep, + }; use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; + use prometeu_system::task::TaskId; use std::io::{Read, Write}; use std::net::TcpStream; @@ -518,6 +523,17 @@ mod tests { } } + #[test] + fn worker_frame_presentation_is_game_only() { + assert!(should_present_worker_frame(&FirmwareState::GameRunning(GameRunningStep::new( + TaskId(1), + )))); + assert!(!should_present_worker_frame(&FirmwareState::ShellRunning(ShellRunningStep::new( + TaskId(2) + ),))); + assert!(!should_present_worker_frame(&FirmwareState::HubHome(HubHomeStep))); + } + #[test] fn host_debugger_maps_cert_events_from_host_owned_sources() { let telemetry = TelemetryFrame { diff --git a/discussion/index.ndjson b/discussion/index.ndjson index d41e1fad..c4b2a834 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-16","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"open","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-16","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md b/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md index 40fa25cf..35253868 100644 --- a/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md +++ b/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md @@ -2,7 +2,7 @@ id: PLN-0121 ticket: real-render-worker-establishment title: Final Worker Path Validation and Hardening -status: open +status: done created: 2026-06-15 completed: ref_decisions: [DEC-0033] @@ -91,3 +91,12 @@ Prove the real render worker path satisfies DEC-0033 across HAL, system, drivers - Final validation may expose earlier plan gaps; fix them in the narrowest affected plan/module. - Manual host evidence can be flaky if tied to native window availability; keep automated host-unit evidence as the primary gate. + +## Validation Evidence + +- `cargo test --workspace`: passed. +- `cargo test -p prometeu-system -p prometeu-drivers -p prometeu-hal -p prometeu-firmware -p prometeu-host-desktop-winit`: passed. +- Native API coupling scan for worker/runtime console code: no `winit`, `pixels`, SDL, swapchain, or native texture API references found. +- Mutable boundary scan for worker/runtime console code: no `&mut Hardware`, `&mut Gfx`, or concrete live `FrameComposer` references found. +- Timing scan for render worker runtime tests: no `thread::sleep` / `sleep(` usage found. +- Added host-unit evidence that worker frame presentation is restricted to `GameRunning`; Shell/Hub paths keep local presentation behavior. From 21b5d59a7ddb8cb6f0a4aabae154fe88729dc925 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 20 Jun 2026 09:05:01 +0100 Subject: [PATCH 47/47] Real Render Worker Establishment --- .../src/local_render_backend.rs | 12 +- .../prometeu-hal/src/syscalls/tests.rs | 10 +- crates/console/prometeu-hal/src/telemetry.rs | 12 +- crates/console/prometeu-system/src/lib.rs | 4 +- .../prometeu-system/src/os/facades/vm.rs | 12 +- .../src/services/vm_runtime/lifecycle.rs | 22 +- .../src/services/vm_runtime/mod.rs | 4 +- .../src/services/vm_runtime/render_worker.rs | 105 +++++-- .../src/services/vm_runtime/tick.rs | 7 +- crates/console/prometeu-vm/src/heap.rs | 40 +-- crates/console/prometeu-vm/src/verifier.rs | 25 +- .../prometeu-vm/src/virtual_machine.rs | 109 ++++--- .../tests/no_legacy.rs | 2 +- .../prometeu-host-desktop-winit/src/runner.rs | 66 ++-- discussion/index.ndjson | 4 +- ...0049-render-worker-publication-boundary.md | 107 +++++++ ...st-hardware-render-boundary-preparation.md | 191 ----------- ...D-0043-real-render-worker-establishment.md | 241 -------------- ...rm-layer-and-hardwarebridge-elimination.md | 198 ------------ .../DEC-0033-real-render-worker-contract.md | 296 ------------------ ...N-0098-define-platform-facade-contracts.md | 81 ----- ...-introduce-owned-render-submission-sink.md | 82 ----- ...igrate-local-render-publication-to-sink.md | 80 ----- ...-remove-immediate-gfx-syscall-rendering.md | 82 ----- ...introduce-game2d-frame-composer-service.md | 82 ----- ...oduce-runtime-platform-and-testplatform.md | 82 ----- ...rate-vm-runtime-hostcontext-to-platform.md | 79 ----- ...e-firmware-and-hub-to-platform-services.md | 81 ----- ...grate-desktop-host-to-platform-services.md | 79 ----- ...0107-migrate-remaining-platform-domains.md | 89 ------ .../PLN-0108-migrate-tests-to-testplatform.md | 81 ----- ...-remove-hardwarebridge-and-update-specs.md | 91 ------ ...10-define-owned-rgba8888-frame-contract.md | 79 ----- ...define-read-only-render-resource-access.md | 79 ----- ...fine-render-worker-errors-and-telemetry.md | 78 ----- ...plement-thread-safe-latest-wins-handoff.md | 80 ----- ...eterministic-render-worker-test-harness.md | 78 ----- ...ment-render-worker-controller-lifecycle.md | 87 ----- ...116-implement-fake-local-render-backend.md | 78 ----- ...ntegrate-runtime-submission-with-worker.md | 80 ----- ...d-stale-epoch-and-repeat-frame-behavior.md | 85 ----- ...egrate-desktop-host-worker-presentation.md | 81 ----- ...LN-0120-update-real-render-worker-specs.md | 79 ----- ...al-worker-path-validation-and-hardening.md | 102 ------ 44 files changed, 350 insertions(+), 3092 deletions(-) create mode 100644 discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md delete mode 100644 discussion/workflow/agendas/AGD-0042-host-hardware-render-boundary-preparation.md delete mode 100644 discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md delete mode 100644 discussion/workflow/decisions/DEC-0032-platform-layer-and-hardwarebridge-elimination.md delete mode 100644 discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md delete mode 100644 discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md delete mode 100644 discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md delete mode 100644 discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md delete mode 100644 discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md delete mode 100644 discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md delete mode 100644 discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md delete mode 100644 discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md delete mode 100644 discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md delete mode 100644 discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md delete mode 100644 discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md delete mode 100644 discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md delete mode 100644 discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md delete mode 100644 discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md delete mode 100644 discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md delete mode 100644 discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md delete mode 100644 discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md delete mode 100644 discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md delete mode 100644 discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md delete mode 100644 discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md delete mode 100644 discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md delete mode 100644 discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md delete mode 100644 discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md delete mode 100644 discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md delete mode 100644 discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md diff --git a/crates/console/prometeu-drivers/src/local_render_backend.rs b/crates/console/prometeu-drivers/src/local_render_backend.rs index 2bed802c..8cbc1be3 100644 --- a/crates/console/prometeu-drivers/src/local_render_backend.rs +++ b/crates/console/prometeu-drivers/src/local_render_backend.rs @@ -123,11 +123,13 @@ mod tests { #[test] fn local_backend_renders_game2d_submission_to_owned_frame() { let backend = backend(); - let mut packet = Game2DFramePacket::default(); - packet.gfx2d = vec![Gfx2dCommand::FillRect { - rect: Rect { x: 1, y: 1, w: 2, h: 2 }, - color: Color::RED, - }]; + let packet = Game2DFramePacket { + gfx2d: vec![Gfx2dCommand::FillRect { + rect: Rect { x: 1, y: 1, w: 2, h: 2 }, + color: Color::RED, + }], + ..Default::default() + }; let ownership = RenderOwnership::new(3, AppMode::Game, 9); let submission = RenderSubmission::game2d(FrameId::new(5), packet).with_ownership(ownership); diff --git a/crates/console/prometeu-hal/src/syscalls/tests.rs b/crates/console/prometeu-hal/src/syscalls/tests.rs index 64e7434a..3625b5ad 100644 --- a/crates/console/prometeu-hal/src/syscalls/tests.rs +++ b/crates/console/prometeu-hal/src/syscalls/tests.rs @@ -69,10 +69,7 @@ fn resolver_rejects_removed_legacy_gfx_set_sprite_identity() { let requested = [SyscallIdentity { module: "gfx2d", name: "set_sprite", version: 1 }]; let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err(); - assert_eq!( - err, - LoadError::UnknownSyscall { module: "gfx2d".into(), name: "set_sprite".into(), version: 1 } - ); + assert_eq!(err, LoadError::UnknownSyscall { module: "gfx2d", name: "set_sprite", version: 1 }); } #[test] @@ -272,10 +269,7 @@ fn resolver_rejects_removed_bank_slot_info_identity() { let requested = [SyscallIdentity { module: "bank", name: "slot_info", version: 1 }]; let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err(); - assert_eq!( - err, - LoadError::UnknownSyscall { module: "bank".into(), name: "slot_info".into(), version: 1 } - ); + assert_eq!(err, LoadError::UnknownSyscall { module: "bank", name: "slot_info", version: 1 }); } #[test] diff --git a/crates/console/prometeu-hal/src/telemetry.rs b/crates/console/prometeu-hal/src/telemetry.rs index 405e3f1f..d74f6be2 100644 --- a/crates/console/prometeu-hal/src/telemetry.rs +++ b/crates/console/prometeu-hal/src/telemetry.rs @@ -405,11 +405,13 @@ mod tests { }; let cert = Certifier::new(config); - let mut tel = TelemetryFrame::default(); - tel.cycles_used = 150; - tel.syscalls = 10; - tel.host_cpu_time_us = 500; - tel.glyph_slots_used = 2; + let tel = TelemetryFrame { + cycles_used: 150, + syscalls: 10, + host_cpu_time_us: 500, + glyph_slots_used: 2, + ..Default::default() + }; let violations = cert.evaluate(&tel, &mut ls, 1000); assert_eq!(violations, 3); diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 8822e7ea..98c39b85 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -11,7 +11,7 @@ pub use services::fs; pub use services::process; pub use services::task; pub use services::vm_runtime::{ - RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff, RenderWorkerHandoffWait, - RenderWorkerOwnership, VirtualMachineRuntime, + LatestRenderFrameStore, RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff, + RenderWorkerHandoffWait, RenderWorkerOwnership, VirtualMachineRuntime, }; pub use services::windows; diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index 20cecd80..5335aab6 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -5,8 +5,8 @@ use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; use prometeu_hal::{ - InputSignals, OwnedRgba8888Frame, RenderWorkerBackend, RenderWorkerError, - RenderWorkerFrameSink, RuntimePlatform, + FrameId, InputSignals, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, + RuntimePlatform, }; use prometeu_vm::VirtualMachine; use std::sync::atomic::Ordering; @@ -116,12 +116,8 @@ impl<'a> VmFacade<'a> { self.os.vm_runtime.render_worker_controller.as_ref() } - pub fn latest_render_worker_frame(&self) -> Option { - self.os.vm_runtime.latest_render_worker_frame() - } - - pub fn repeat_latest_render_worker_frame(&self) -> Option { - self.os.vm_runtime.repeat_latest_render_worker_frame() + pub fn record_repeated_render_worker_frame(&self, frame_id: FrameId) { + self.os.vm_runtime.record_repeated_render_worker_frame(frame_id); } pub fn cert_config(&self) -> &CertificationConfig { diff --git a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs index 7463e0c1..a2c5ee2a 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs @@ -3,9 +3,7 @@ use crate::CrashReport; use crate::fs::{FsBackend, FsState, VirtualFS}; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::log::{LogLevel, LogSource}; -use prometeu_hal::{ - OwnedRgba8888Frame, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, -}; +use prometeu_hal::{FrameId, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink}; use std::collections::HashMap; impl VirtualMachineRuntime { @@ -183,24 +181,14 @@ impl VirtualMachineRuntime { } } - pub(crate) fn publish_pending_render_to_worker(&mut self) -> bool { - let Some(submission) = self.render_manager.take_pending_for_worker() else { - return false; - }; - self.render_worker_handoff.publish(submission); - true - } - pub(crate) fn sync_render_worker_ownership(&self) { self.render_worker_ownership.set(self.render_manager.active_ownership()); } - pub fn latest_render_worker_frame(&self) -> Option { - self.render_worker_controller.as_ref()?.latest_published_frame() - } - - pub fn repeat_latest_render_worker_frame(&self) -> Option { - self.render_worker_controller.as_ref()?.repeat_latest_frame() + pub fn record_repeated_render_worker_frame(&self, frame_id: FrameId) { + if let Some(controller) = self.render_worker_controller.as_ref() { + controller.record_repeated_frame(frame_id); + } } pub fn initialize_vm( diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index 5312ec3f..6db48101 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -21,7 +21,9 @@ use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier}; use prometeu_hal::{Gfx2dCommand, GfxUiCommand}; use prometeu_vm::VirtualMachine; pub use render_manager::{RenderManager, RenderRuntimeCapabilities}; -pub use render_worker::{RenderWorkerConfig, RenderWorkerController, RenderWorkerOwnership}; +pub use render_worker::{ + LatestRenderFrameStore, RenderWorkerConfig, RenderWorkerController, RenderWorkerOwnership, +}; pub use render_worker_handoff::{RenderWorkerHandoff, RenderWorkerHandoffWait}; use std::collections::HashMap; use std::sync::Arc; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs index 7993ec8f..3be9b2dc 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs @@ -25,7 +25,28 @@ impl Default for RenderWorkerConfig { struct RenderWorkerState { telemetry: RenderWorkerTelemetry, last_error: Option, - latest_published_frame: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct LatestRenderFrameStore { + latest: Arc>>, +} + +impl LatestRenderFrameStore { + pub fn publish_frame(&self, frame: OwnedRgba8888Frame) { + *self.latest.lock().unwrap() = Some(frame); + } + + pub fn latest_frame(&self) -> Option { + self.latest.lock().unwrap().clone() + } +} + +impl RenderWorkerFrameSink for LatestRenderFrameStore { + fn publish(&self, frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError> { + self.publish_frame(frame); + Ok(()) + } } #[derive(Clone, Debug)] @@ -100,11 +121,11 @@ impl RenderWorkerController { self.handoff.request_shutdown(); match self.done_rx.recv_timeout(self.config.shutdown_timeout) { Ok(result) => { - if let Some(handle) = self.handle.take() { - if handle.join().is_err() { - record_error(&self.state, RenderWorkerError::WorkerPanic, FrameId::ZERO); - return Err(RenderWorkerError::WorkerPanic); - } + if let Some(handle) = self.handle.take() + && handle.join().is_err() + { + record_error(&self.state, RenderWorkerError::WorkerPanic, FrameId::ZERO); + return Err(RenderWorkerError::WorkerPanic); } result } @@ -145,15 +166,9 @@ impl RenderWorkerController { self.ownership.snapshot() } - pub fn latest_published_frame(&self) -> Option { - self.state.lock().unwrap().latest_published_frame.clone() - } - - pub fn repeat_latest_frame(&self) -> Option { + pub fn record_repeated_frame(&self, frame_id: FrameId) { let mut state = self.state.lock().unwrap(); - let frame = state.latest_published_frame.clone()?; - state.telemetry.record_repeated_frame(frame.frame_id.get()); - Some(frame) + state.telemetry.record_repeated_frame(frame_id.get()); } } @@ -197,19 +212,18 @@ fn run_worker_loop( continue; } - let publish_result = catch_unwind(AssertUnwindSafe(|| sink.publish(frame.clone()))) + let publish_result = catch_unwind(AssertUnwindSafe(|| sink.publish(frame))) .map_err(|_| RenderWorkerError::WorkerPanic); match publish_result { - Ok(Ok(())) => record_published(&state, frame), + Ok(Ok(())) => record_published(&state, frame_id), Ok(Err(error)) | Err(error) => record_error(&state, error, frame_id), } } } -fn record_published(state: &Arc>, frame: OwnedRgba8888Frame) { +fn record_published(state: &Arc>, frame_id: FrameId) { let mut state = state.lock().unwrap(); - state.telemetry.record_published(frame.frame_id.get()); - state.latest_published_frame = Some(frame); + state.telemetry.record_published(frame_id.get()); } fn record_stale_discard(state: &Arc>, frame_id: FrameId) { @@ -278,6 +292,35 @@ mod tests { } } + #[test] + fn latest_render_frame_store_is_sink_and_observable_publication_source() { + let store = LatestRenderFrameStore::default(); + let first = OwnedRgba8888Frame::packed( + FrameId::new(1), + RenderOwnership::new(1, AppMode::Game, 1), + 1, + 1, + vec![0xFF0000FF], + ) + .expect("first frame"); + let second = OwnedRgba8888Frame::packed( + FrameId::new(2), + RenderOwnership::new(1, AppMode::Game, 1), + 1, + 1, + vec![0x00FF00FF], + ) + .expect("second frame"); + + store.publish(first).expect("publish first"); + assert_eq!(store.latest_frame().expect("first latest").frame_id, FrameId::new(1)); + + store.publish(second).expect("publish second"); + let latest = store.latest_frame().expect("second latest"); + assert_eq!(latest.frame_id, FrameId::new(2)); + assert_eq!(latest.pixels, vec![0x00FF00FF]); + } + #[test] fn render_worker_controller_starts_and_stops_waiting_worker() { let handoff = Arc::new(RenderWorkerHandoff::default()); @@ -387,27 +430,37 @@ mod tests { fn render_worker_controller_repeats_latest_published_frame_without_rendering() { let handoff = Arc::new(RenderWorkerHandoff::default()); let backend = Arc::new(FakeRenderBackend::default()); + let store = LatestRenderFrameStore::default(); let mut controller = RenderWorkerController::start( RenderWorkerConfig::default(), Arc::clone(&handoff), RenderWorkerOwnership::new(RenderOwnership::new(1, AppMode::Game, 1)), SharedFakeBackend(Arc::clone(&backend)), - SharedFakeBackend(Arc::clone(&backend)), + store.clone(), ); handoff.publish(game_submission(22, 1, 1)); assert_eq!(controller.stop(), Ok(())); - let latest = controller.latest_published_frame().expect("latest frame"); - let repeated = controller.repeat_latest_frame().expect("repeated frame"); + let latest = store.latest_frame().expect("latest frame"); + controller.record_repeated_frame(latest.frame_id); + let repeated = store.latest_frame().expect("repeated frame"); assert_eq!(latest, repeated); - assert_eq!(backend.published_frames().len(), 1); + assert!(backend.published_frames().is_empty()); assert_eq!(controller.telemetry().repeated_frames, 1); } #[test] fn runtime_publishes_game_submissions_to_worker_without_waiting_for_render() { + fn publish_pending(runtime: &mut VirtualMachineRuntime) -> bool { + let Some(submission) = runtime.render_manager.take_pending_for_worker() else { + return false; + }; + runtime.render_worker_handoff.publish(submission); + true + } + let gate = RenderGate::default(); let backend = Arc::new(FakeRenderBackend::with_gate(gate.clone())); let mut runtime = VirtualMachineRuntime::new(None); @@ -423,7 +476,7 @@ mod tests { Game2DFramePacket::default(), )) .expect("game packet should close"); - assert!(runtime.publish_pending_render_to_worker()); + assert!(publish_pending(&mut runtime)); gate.wait_for_entries(1); runtime @@ -432,14 +485,14 @@ mod tests { Game2DFramePacket::default(), )) .expect("second game packet should close"); - assert!(runtime.publish_pending_render_to_worker()); + assert!(publish_pending(&mut runtime)); runtime .render_manager .close_frame_with_packet(prometeu_hal::RenderSubmissionPacket::Game2D( Game2DFramePacket::default(), )) .expect("third game packet should close"); - assert!(runtime.publish_pending_render_to_worker()); + assert!(publish_pending(&mut runtime)); assert_eq!(runtime.render_worker_handoff.telemetry().replaced_before_consume, 1); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index 78da52bd..86716bbc 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -121,6 +121,7 @@ impl VirtualMachineRuntime { } } + #[allow(clippy::too_many_arguments)] pub fn tick( &mut self, log_service: &mut LogService, @@ -276,11 +277,7 @@ impl VirtualMachineRuntime { if self.render_worker_controller.is_some() && self.current_cartridge_app_mode == AppMode::Game => { - if self.publish_pending_render_to_worker() { - RenderConsumeOutcome::NoSubmission - } else { - RenderConsumeOutcome::NoSubmission - } + RenderConsumeOutcome::NoSubmission } RenderConsumerPath::RealWorker => { self.render_manager.consume_latest(&mut surface) diff --git a/crates/console/prometeu-vm/src/heap.rs b/crates/console/prometeu-vm/src/heap.rs index 9deb2fe5..bc361fa9 100644 --- a/crates/console/prometeu-vm/src/heap.rs +++ b/crates/console/prometeu-vm/src/heap.rs @@ -548,17 +548,17 @@ mod tests { // replace with arrays containing cross-references. Since our simple // heap doesn't support in-place element edits via API, simulate by // directly editing stored objects. - if let Some(slot) = heap.objects.get_mut(a.0 as usize) { - if let Some(obj) = slot.as_mut() { - obj.array_elems = Some(vec![Value::HeapRef(b)]); - obj.header.payload_len = 1; - } + if let Some(slot) = heap.objects.get_mut(a.0 as usize) + && let Some(obj) = slot.as_mut() + { + obj.array_elems = Some(vec![Value::HeapRef(b)]); + obj.header.payload_len = 1; } - if let Some(slot) = heap.objects.get_mut(b.0 as usize) { - if let Some(obj) = slot.as_mut() { - obj.array_elems = Some(vec![Value::HeapRef(a)]); - obj.header.payload_len = 1; - } + if let Some(slot) = heap.objects.get_mut(b.0 as usize) + && let Some(obj) = slot.as_mut() + { + obj.array_elems = Some(vec![Value::HeapRef(a)]); + obj.header.payload_len = 1; } // Mark from A; should terminate and mark both. @@ -720,17 +720,17 @@ mod tests { let b = heap.allocate_array(vec![]); // Make A point to B and B point to A. - if let Some(slot) = heap.objects.get_mut(a.0 as usize) { - if let Some(obj) = slot.as_mut() { - obj.array_elems = Some(vec![Value::HeapRef(b)]); - obj.header.payload_len = 1; - } + if let Some(slot) = heap.objects.get_mut(a.0 as usize) + && let Some(obj) = slot.as_mut() + { + obj.array_elems = Some(vec![Value::HeapRef(b)]); + obj.header.payload_len = 1; } - if let Some(slot) = heap.objects.get_mut(b.0 as usize) { - if let Some(obj) = slot.as_mut() { - obj.array_elems = Some(vec![Value::HeapRef(a)]); - obj.header.payload_len = 1; - } + if let Some(slot) = heap.objects.get_mut(b.0 as usize) + && let Some(obj) = slot.as_mut() + { + obj.array_elems = Some(vec![Value::HeapRef(a)]); + obj.header.payload_len = 1; } // No roots: perform sweep directly; both should be reclaimed. diff --git a/crates/console/prometeu-vm/src/verifier.rs b/crates/console/prometeu-vm/src/verifier.rs index a689abc6..1c098ae8 100644 --- a/crates/console/prometeu-vm/src/verifier.rs +++ b/crates/console/prometeu-vm/src/verifier.rs @@ -1052,12 +1052,13 @@ mod tests { // 21: Jmp 27 // 27: Nop - let mut code = Vec::new(); - code.push(OpCode::PushBool as u8); - code.push(0x00); - code.push(1); // 0: PushBool (3 bytes) - code.push(OpCode::JmpIfTrue as u8); - code.push(0x00); + let mut code = vec![ + OpCode::PushBool as u8, + 0x00, + 1, // 0: PushBool (3 bytes) + OpCode::JmpIfTrue as u8, + 0x00, + ]; code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes) code.push(OpCode::Jmp as u8); code.push(0x00); @@ -1247,9 +1248,7 @@ mod tests { #[test] fn test_function_without_terminator_is_rejected() { // Single NOP with no RET/JMP/TRAP/HALT at the end → fallthrough to end - let mut code = Vec::new(); - code.push(OpCode::Nop as u8); - code.push(0x00); + let code = vec![OpCode::Nop as u8, 0x00]; let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }]; let res = Verifier::verify(&code, &functions); @@ -1259,9 +1258,7 @@ mod tests { #[test] fn test_function_with_proper_terminator_passes() { // Minimal function that returns immediately - let mut code = Vec::new(); - code.push(OpCode::Ret as u8); - code.push(0x00); + let code = vec![OpCode::Ret as u8, 0x00]; let functions = vec![FunctionMeta { code_offset: 0, @@ -1277,9 +1274,7 @@ mod tests { fn test_verifier_ret_too_few_slots() { // Function declares 1 return slot but returns nothing // 0: Ret - let mut code = Vec::new(); - code.push(OpCode::Ret as u8); - code.push(0x00); + let code = vec![OpCode::Ret as u8, 0x00]; let functions = vec![FunctionMeta { code_offset: 0, diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index 7cbd7a0a..8f5d6002 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -1596,9 +1596,10 @@ mod tests { #[test] fn test_push_f64_immediate() { + let value = 3.125f64; let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushF64 as u16).to_le_bytes()); - rom.extend_from_slice(&3.14f64.to_le_bytes()); + rom.extend_from_slice(&value.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); @@ -1606,7 +1607,7 @@ mod tests { let mut ctx = HostContext::new(None); vm.step(&mut native, &mut ctx).unwrap(); - assert_eq!(vm.peek().unwrap(), &Value::Float(3.14)); + assert_eq!(vm.peek().unwrap(), &Value::Float(value)); } #[test] @@ -2537,9 +2538,8 @@ mod tests { let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); // Any non-trap outcome is considered success here - match report.reason { - LogicalFrameEndingReason::Trap(trap) => panic!("Unexpected trap: {:?}", trap), - _ => {} + if let LogicalFrameEndingReason::Trap(trap) = report.reason { + panic!("Unexpected trap: {:?}", trap); } } @@ -2672,8 +2672,7 @@ mod tests { #[test] fn test_loader_hardening_successful_init() { - let mut vm = VirtualMachine::default(); - vm.pc = 123; // Pollution + let mut vm = VirtualMachine { pc: 123, ..Default::default() }; let code = assemble("HALT").expect("assemble"); let header = prometeu_bytecode::model::BytecodeModule { @@ -3978,26 +3977,26 @@ mod tests { let mut a_href = None; let mut b_href = None; // Consider currently running coroutine - if let Some(cur) = vm.current_coro { - if let Some(f) = vm.call_stack.last() { - if f.func_idx == 1 { - a_href = Some(cur); - } - if f.func_idx == 2 { - b_href = Some(cur); - } + if let Some(cur) = vm.current_coro + && let Some(f) = vm.call_stack.last() + { + if f.func_idx == 1 { + a_href = Some(cur); + } + if f.func_idx == 2 { + b_href = Some(cur); } } // And also consider suspended (Ready/Sleeping) coroutines for h in vm.heap.suspended_coroutine_handles() { - if let Some(co) = vm.heap.coroutine_data(h) { - if let Some(f) = co.frames.last() { - if f.func_idx == 1 { - a_href = Some(h); - } - if f.func_idx == 2 { - b_href = Some(h); - } + if let Some(co) = vm.heap.coroutine_data(h) + && let Some(f) = co.frames.last() + { + if f.func_idx == 1 { + a_href = Some(h); + } + if f.func_idx == 2 { + b_href = Some(h); } } } @@ -4108,25 +4107,25 @@ mod tests { // Identify A and B coroutine handles (consider both running and suspended) let mut a_href = None; let mut b_href = None; - if let Some(cur) = vm.current_coro { - if let Some(f) = vm.call_stack.last() { - if f.func_idx == 1 { - a_href = Some(cur); - } - if f.func_idx == 2 { - b_href = Some(cur); - } + if let Some(cur) = vm.current_coro + && let Some(f) = vm.call_stack.last() + { + if f.func_idx == 1 { + a_href = Some(cur); + } + if f.func_idx == 2 { + b_href = Some(cur); } } for h in vm.heap.suspended_coroutine_handles() { - if let Some(co) = vm.heap.coroutine_data(h) { - if let Some(f) = co.frames.last() { - if f.func_idx == 1 { - a_href = Some(h); - } - if f.func_idx == 2 { - b_href = Some(h); - } + if let Some(co) = vm.heap.coroutine_data(h) + && let Some(f) = co.frames.last() + { + if f.func_idx == 1 { + a_href = Some(h); + } + if f.func_idx == 2 { + b_href = Some(h); } } } @@ -4253,26 +4252,26 @@ mod tests { let mut a = None; let mut b = None; // running - if let Some(cur) = vm.current_coro { - if let Some(f) = vm.call_stack.last() { - if f.func_idx == 1 { - a = Some(cur); - } - if f.func_idx == 2 { - b = Some(cur); - } + if let Some(cur) = vm.current_coro + && let Some(f) = vm.call_stack.last() + { + if f.func_idx == 1 { + a = Some(cur); + } + if f.func_idx == 2 { + b = Some(cur); } } // suspended for h in vm.heap.suspended_coroutine_handles() { - if let Some(co) = vm.heap.coroutine_data(h) { - if let Some(f) = co.frames.last() { - if f.func_idx == 1 { - a = Some(h); - } - if f.func_idx == 2 { - b = Some(h); - } + if let Some(co) = vm.heap.coroutine_data(h) + && let Some(f) = co.frames.last() + { + if f.func_idx == 1 { + a = Some(h); + } + if f.func_idx == 2 { + b = Some(h); } } } diff --git a/crates/dev/prometeu-quality-checks/tests/no_legacy.rs b/crates/dev/prometeu-quality-checks/tests/no_legacy.rs index 68ef76db..a7bff026 100644 --- a/crates/dev/prometeu-quality-checks/tests/no_legacy.rs +++ b/crates/dev/prometeu-quality-checks/tests/no_legacy.rs @@ -168,7 +168,7 @@ fn path_segment_violation(path: &Path) -> Option { } fn is_forbidden_ident(tok: &str) -> bool { - FORBIDDEN_IDENT_TOKENS.iter().any(|&bad| bad == tok) + FORBIDDEN_IDENT_TOKENS.contains(&tok) } fn tokenize_identifiers(text: &str) -> Vec<(String, usize, usize)> { diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index 5c4871f3..feb56c4a 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -10,9 +10,9 @@ use pixels::{Pixels, PixelsBuilder, SurfaceTexture}; use prometeu_drivers::hardware::Hardware; use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks}; use prometeu_firmware::{BootTarget, Firmware, FirmwareState}; +use prometeu_hal::RuntimePlatform; use prometeu_hal::telemetry::CertificationConfig; -use prometeu_hal::{OwnedRgba8888Frame, RenderWorkerError, RenderWorkerFrameSink, RuntimePlatform}; -use prometeu_system::RenderWorkerConfig; +use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig}; use std::sync::Arc; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; @@ -24,15 +24,6 @@ use winit::window::{Window, WindowAttributes, WindowId}; const IDLE_HOST_POLL_DT: Duration = Duration::from_millis(100); -#[derive(Debug, Clone, Copy)] -struct HostWorkerFrameSink; - -impl RenderWorkerFrameSink for HostWorkerFrameSink { - fn publish(&self, _frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError> { - Ok(()) - } -} - #[derive(Debug, Clone)] struct PresentationState { latest_published_frame: u64, @@ -147,6 +138,8 @@ pub struct HostRunner { audio: HostAudio, /// Last known pause state to sync with audio. last_paused_state: bool, + /// Worker-published frame store used as the single observable publication source. + render_frame_store: LatestRenderFrameStore, /// Tracks whether a new logical frame or host-only surface invalidation requires presentation. presentation: PresentationState, } @@ -177,10 +170,11 @@ impl HostRunner { memory_banks, hardware.assets.glyph_asset_slot_index(), ); + let render_frame_store = LatestRenderFrameStore::default(); firmware.os.vm().start_render_worker( RenderWorkerConfig::default(), worker_backend, - HostWorkerFrameSink, + render_frame_store.clone(), ); Self { @@ -199,6 +193,7 @@ impl HostRunner { debugger: HostDebugger::new(), audio: HostAudio::new(), last_paused_state: false, + render_frame_store, presentation: PresentationState::default(), } } @@ -252,7 +247,9 @@ impl ApplicationHandler for HostRunner { let window = event_loop.create_window(attrs).expect("failed to create window"); - // 🔥 Leak: Window becomes &'static Window (bootstrap) + // `pixels` stores a surface tied to the window lifetime. The current + // winit runner owns one process-lifetime window, so we intentionally + // promote it to a static reference for the duration of the host process. let window: &'static Window = Box::leak(Box::new(window)); self.window = Some(window); @@ -261,7 +258,7 @@ impl ApplicationHandler for HostRunner { let (display_w, display_h) = self.hardware.display_size(); let mut pixels = PixelsBuilder::new(display_w as u32, display_h as u32, surface_texture) - .present_mode(PresentMode::Fifo) // activate vsync + .present_mode(PresentMode::Fifo) .build() .expect("failed to create Pixels"); @@ -298,23 +295,24 @@ impl ApplicationHandler for HostRunner { } WindowEvent::RedrawRequested => { - // Get Pixels directly from the field (not via helper that gets the entire &mut self) let pixels = self.pixels.as_mut().expect("pixels not initialized"); { - // Mutable borrow of the frame (lasts only within this block) let frame = pixels.frame_mut(); if should_present_worker_frame(&self.firmware.state) - && let Some(worker_frame) = - self.firmware.os.vm().repeat_latest_render_worker_frame() + && let Some(worker_frame) = self.render_frame_store.latest_frame() { + self.firmware + .os + .vm() + .record_repeated_render_worker_frame(worker_frame.frame_id); draw_owned_rgba8888_frame_to_rgba8(&worker_frame, frame); } else { let src = self.hardware.gfx.front_buffer(); draw_rgba8888_to_rgba8(src, frame); } - } // <- frame borrow ends here + } if pixels.render().is_err() { event_loop.exit(); @@ -378,7 +376,7 @@ impl ApplicationHandler for HostRunner { self.last_frame_time = now; self.accumulator += frame_delta; - // 🔥 Logic Update Loop: consumes time in exact 60Hz (16.66ms) slices. + // Consume time in exact 60Hz slices. while self.accumulator >= self.frame_target_dt { // Unless the debugger is waiting for a 'start' command, advance the system. if !self.debugger.waiting_for_start { @@ -405,7 +403,7 @@ impl ApplicationHandler for HostRunner { } if should_present_worker_frame(&self.firmware.state) - && let Some(worker_frame) = self.firmware.os.vm().latest_render_worker_frame() + && let Some(worker_frame) = self.render_frame_store.latest_frame() { self.presentation.note_published_frame(worker_frame.frame_id.get()); } else { @@ -453,8 +451,10 @@ mod tests { use prometeu_firmware::firmware::firmware_state::{ GameRunningStep, HubHomeStep, ShellRunningStep, }; + use prometeu_hal::app_mode::AppMode; use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; + use prometeu_hal::{FrameId, OwnedRgba8888Frame, RenderOwnership, RenderWorkerFrameSink}; use prometeu_system::task::TaskId; use std::io::{Read, Write}; use std::net::TcpStream; @@ -534,6 +534,30 @@ mod tests { assert!(!should_present_worker_frame(&FirmwareState::HubHome(HubHomeStep))); } + #[test] + fn host_presentation_observes_worker_publication_store() { + let store = LatestRenderFrameStore::default(); + let frame = OwnedRgba8888Frame::packed( + FrameId::new(9), + RenderOwnership::new(1, AppMode::Game, 1), + 1, + 1, + vec![0xFF00FFFF], + ) + .expect("test frame"); + + store.publish(frame).expect("store publish"); + + let mut presentation = PresentationState::default(); + presentation.mark_presented(); + let published = store.latest_frame().expect("published frame"); + presentation.note_published_frame(published.frame_id.get()); + + assert!(presentation.should_request_redraw()); + presentation.mark_presented(); + assert_eq!(presentation.last_presented_frame, Some(9)); + } + #[test] fn host_debugger_maps_cert_events_from_host_owned_sources() { let telemetry = TelemetryFrame { diff --git a/discussion/index.ndjson b/discussion/index.ndjson index c4b2a834..f2a5c205 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":34,"PLN":122,"LSN":49,"CLSN":1}} +{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":34,"PLN":123,"LSN":50,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0042","status":"in_progress","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-16","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-host-hardware-render-boundary-preparation.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06"},{"id":"AGD-0043","file":"AGD-0043-real-render-worker-establishment.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-15"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-platform-layer-and-hardwarebridge-elimination.md","status":"accepted","created_at":"2026-06-06","updated_at":"2026-06-06","ref_agenda":"AGD-0042"},{"id":"DEC-0033","file":"DEC-0033-real-render-worker-contract.md","status":"accepted","created_at":"2026-06-15","updated_at":"2026-06-15","ref_agenda":"AGD-0043"}],"plans":[{"id":"PLN-0098","file":"PLN-0098-define-platform-facade-contracts.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0099","file":"PLN-0099-introduce-owned-render-submission-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0100","file":"PLN-0100-migrate-local-render-publication-to-sink.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0101","file":"PLN-0101-remove-immediate-gfx-syscall-rendering.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0102","file":"PLN-0102-introduce-game2d-frame-composer-service.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0103","file":"PLN-0103-introduce-runtime-platform-and-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0104","file":"PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0105","file":"PLN-0105-migrate-firmware-and-hub-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0106","file":"PLN-0106-migrate-desktop-host-to-platform-services.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0107","file":"PLN-0107-migrate-remaining-platform-domains.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0108","file":"PLN-0108-migrate-tests-to-testplatform.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0109","file":"PLN-0109-remove-hardwarebridge-and-update-specs.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-15","ref_decisions":["DEC-0032"]},{"id":"PLN-0110","file":"PLN-0110-define-owned-rgba8888-frame-contract.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0111","file":"PLN-0111-define-read-only-render-resource-access.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0112","file":"PLN-0112-define-render-worker-errors-and-telemetry.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0113","file":"PLN-0113-implement-thread-safe-latest-wins-handoff.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0114","file":"PLN-0114-add-deterministic-render-worker-test-harness.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0115","file":"PLN-0115-implement-render-worker-controller-lifecycle.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0116","file":"PLN-0116-implement-fake-local-render-backend.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0117","file":"PLN-0117-integrate-runtime-submission-with-worker.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-15","ref_decisions":["DEC-0033"]},{"id":"PLN-0118","file":"PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0119","file":"PLN-0119-integrate-desktop-host-worker-presentation.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0120","file":"PLN-0120-update-real-render-worker-specs.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]},{"id":"PLN-0121","file":"PLN-0121-final-worker-path-validation-and-hardening.md","status":"done","created_at":"2026-06-15","updated_at":"2026-06-16","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md b/discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md new file mode 100644 index 00000000..17bb3fbe --- /dev/null +++ b/discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md @@ -0,0 +1,107 @@ +--- +id: LSN-0049 +ticket: real-render-worker-establishment +title: Render Workers Publish Frames, Hosts Present Them +created: 2026-06-20 +tags: [runtime, renderer, worker, host, publication, concurrency] +--- + +## Context + +The real render worker work moved Prometeu from local, synchronous rendering toward an asynchronous worker path. The important architectural question was not only how to create a thread, but where ownership of render work, frame publication, and native presentation should live. + +The final model keeps the VM producer, render worker, and desktop presenter separated: + +- the VM closes logical render submissions; +- the worker consumes owned submissions through a latest-wins handoff; +- the worker publishes an owned `OwnedRgba8888Frame`; +- the desktop host uploads and presents the latest published frame through native window APIs. + +This boundary is what makes the worker real without making it desktop-specific. + +## Key Decisions + +### Platform Boundary Before Worker Boundary + +**What:** Prometeu removed the monolithic hardware bridge from runtime-facing contracts and introduced explicit platform services before establishing the real worker. + +**Why:** A render worker cannot safely consume `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state. Those are single-threaded runtime concerns. The worker needs closed data plus read-only resource access. + +**Trade-offs:** This required broad preparatory work before the worker could exist. The payoff is that the worker boundary is now testable without a native window and does not depend on host presentation details. + +### Closed Submissions Are The Worker Input + +**What:** The VM producer closes render work into owned `RenderSubmission` values and publishes them through a single-slot latest-wins handoff. + +**Why:** The worker must not observe partially built render state or preserve an unbounded queue of historical frames. If the producer outruns the consumer, the newest complete submission wins. + +**Trade-offs:** Some produced frames are intentionally dropped before consumption. That is correct for this runtime because the contract is visual freshness and VM progress, not rendering every intermediate state. + +### Publication Store Is The Observable Frame Boundary + +**What:** The sink that receives worker-published `OwnedRgba8888Frame` values is also the source the host reads for presentation. + +**Why:** A no-op sink with a separate controller-owned latest-frame cache makes publication ambiguous. The controller should own lifecycle, shutdown, handoff, errors, and telemetry. The publication store should own the latest observable frame. + +**Trade-offs:** Repeat telemetry remains a runtime concern and is recorded by frame id. Pixel retention stays in the publication store so the host can present without depending on controller internals. + +### Host Presentation Remains Native And Single-Threaded + +**What:** The worker never imports or calls winit, pixels, SDL, swapchain, native texture, or window APIs. The host event loop remains responsible for upload and present. + +**Why:** Native presentation APIs often have thread affinity and backend-specific constraints. Keeping them in the host event loop avoids leaking host constraints into the runtime worker contract. + +**Trade-offs:** The worker publishes memory frames rather than presenting directly. The host still performs the final copy/upload step, but the runtime remains portable and testable. + +## Patterns and Algorithms + +### Latest-Wins Handoff + +Use a single pending slot plus a condition variable: + +- producer publish replaces pending work if the worker has not consumed it; +- replacement increments telemetry; +- worker wait takes the latest pending submission; +- in-flight work is separate from pending work; +- producer publication never waits for render completion. + +This is a bounded backpressure model. It prevents queue growth while keeping VM progress independent from render speed. + +### Ownership/Epoch Stale Discard + +Each submission and frame carries render ownership metadata. Before publication, the worker compares the completed frame ownership with the active ownership snapshot. If the frame is stale, it is discarded and counted. + +This prevents obsolete pixels from being presented after an app, mode, or ownership transition. + +### Store-Backed Publication + +The publication sink stores the latest `OwnedRgba8888Frame` in owned memory. The desktop host clones or reads that frame before copying pixels into the native framebuffer. + +The store is intentionally simpler than the controller. It does not own worker lifecycle. It only answers: "what is the latest worker-published frame?" + +## Pitfalls + +### A Worker Thread Alone Is Not A Worker Contract + +Moving code to another thread is not sufficient. The contract must define input ownership, resource access, publication semantics, shutdown, stale-frame behavior, and telemetry. + +### Native Presentation Must Not Leak Back Into Runtime + +It is tempting to let the worker call the host presenter directly. That couples runtime execution to desktop window rules and makes deterministic tests harder. Publish owned frames first; present later in the host. + +### No-Op Sinks Hide Boundary Ambiguity + +A sink that accepts a frame and discards it while another component stores the actual latest frame is a design smell. The object that receives `publish(frame)` should be the observable publication boundary, or the controller should be explicitly documented as that store. Prometeu chose the store-backed sink model. + +### Concurrency Tests Must Publish Before Waiting + +A deterministic gate only works after work has actually reached the worker. Waiting for backend entry before publishing to the handoff causes a pipeline hang. Tests should explicitly publish pending work before waiting on worker-side synchronization. + +## Takeaways + +- Render workers should consume closed, owned submissions, not live runtime state. +- Latest-wins handoff is a deliberate bounded backpressure model, not a queue shortcut. +- In-flight render work must be validated against current ownership before publication. +- Worker publication and host presentation are different responsibilities. +- The sink/store that receives published frames should be the single observable source for presentation. +- Deterministic concurrency tests should synchronize on real state transitions, not sleeps or assumed scheduling. diff --git a/discussion/workflow/agendas/AGD-0042-host-hardware-render-boundary-preparation.md b/discussion/workflow/agendas/AGD-0042-host-hardware-render-boundary-preparation.md deleted file mode 100644 index 5bb4f48c..00000000 --- a/discussion/workflow/agendas/AGD-0042-host-hardware-render-boundary-preparation.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -id: AGD-0042 -ticket: real-render-worker-establishment -title: Host Hardware and Render Boundary Preparation -status: accepted -created: 2026-06-06 -resolved: -decision: DEC-0032 -tags: [runtime, renderer, hardware, host, hal, boundary, architecture] ---- - -## Contexto - -`DSC-0040` fechou a arquitetura base para separar VM/logical execution de render consumption. A implementacao atual ainda usa `HardwareBridge` como uma trait agregadora grande: render, composer, `Gfx`, audio, input, touch e assets ficam expostos juntos para runtime, firmware e syscalls. - -Para estabelecer um worker real, precisamos antes preparar essa fronteira. A direcao agora nao e mais transformar `HardwareBridge` em uma interface host-implemented permanente. A direcao e reformular o runtime em uma platform layer com servicos explicitos e eliminar completamente o `HardwareBridge` monolitico como contrato final. - -O worker real deve depender apenas de uma sub-abstracao minima de render, nunca de `&mut Hardware` inteiro. - -Esta agenda (`AGD-0042`) discute a preparacao/refatoracao de fronteiras. A agenda seguinte (`AGD-0043`) dentro da mesma `DSC-0042` discutira o estabelecimento do worker real propriamente dito. - -## Problema - -Hoje o sistema ja tem uma trait (`HardwareBridge`), mas ela ainda permite acoplamentos que impedem um worker real limpo: - -- `HostContext` entrega `&mut dyn HardwareBridge` para syscalls; -- syscalls `gfx2d.*` e `gfxui.*` ainda fazem buffering de comandos e tambem desenham imediatamente via `hw.gfx_mut()`; -- composer state (`bind_scene`, `set_camera`, `emit_sprite`, `close_game2d_packet`) vive dentro da mesma abstracao que render/present; -- firmware e Hub publicam diretamente via `hw.publish_render_submission`; -- host desktop instancia `prometeu_drivers::Hardware` diretamente e usa constantes concretas como `Hardware::W/H`; -- testes dependem fortemente do `Hardware` concreto. - -Se introduzirmos o worker real agora, a implementacao provavelmente acabara passando `&mut Hardware`, `&mut Gfx` ou `FrameComposer` vivo para a fronteira do worker por conveniencia. Isso quebraria o contrato definido em `DEC-0031`. - -## Pontos Criticos - -### 0. Escopo da migracao completa - -Podemos migrar render primeiro para reduzir risco e desbloquear o worker real, mas a mudanca nao deve parar no render. Audio, input, touch, assets/storage, clock/pacing e telemetry tambem devem sair do `HardwareBridge` monolitico para servicos/facades explicitos. - -`HardwareBridge` deve morrer ao final desta mudanca. Qualquer uso intermediario e scaffold de migracao, nao contrato final. - -### 1. Eliminacao do `HardwareBridge` - -`HardwareBridge` pode existir apenas como detalhe transitorio durante a migracao, se isso reduzir risco de implementacao. Ele nao deve sobreviver como compat layer permanente nem como contrato publico do runtime. - -O estado final deve remover `HardwareBridge` e substituir o acesso monolitico por servicos explicitos de plataforma. - -### 2. Render submission sink - -O primeiro corte provavelmente deve extrair algo como `RenderSubmissionSink`, separando `submit/publish` de `HardwareBridge`. - -### 3. Remocao de render imediato das syscalls - -As syscalls de `gfx2d`/`gfxui` devem apenas gravar comandos nos buffers que fecham o packet. O desenho imediato via `hw.gfx_mut()` deve sair antes do worker real. - -### 4. Composer como dominio logico - -`bind_scene`, `set_camera`, `emit_sprite` e `close_game2d_packet` pertencem ao fechamento logico do frame. Precisamos separar essa capacidade do backend que rasteriza/presenta. - -### 5. Host-owned concrete hardware - -`prometeu_drivers::Hardware` deve continuar existindo como implementacao local/default, mas nao como a arquitetura. O host desktop pode owns essa implementacao ou uma composicao de facades. - -### 6. Test migration - -Muitos testes instanciam `Hardware::new()`. A migracao deve preservar fixtures simples, talvez com um `TestHardware` ou `LocalHardware` que implemente as novas facades. - -## Opcoes - -### Opcao A - Quebrar `HardwareBridge` em uma unica grande refatoracao - -**Abordagem:** substituir `HardwareBridge` por facades menores em runtime, firmware, host e testes de uma vez. - -**Pros:** -- resultado final mais limpo imediatamente; -- remove rapidamente o risco de acoplamento. - -**Contras:** -- alto blast radius; -- muitos testes mudam ao mesmo tempo; -- dificil isolar regressao funcional de regressao arquitetural. - -**Manutenibilidade:** boa no destino, arriscada durante a transicao. - -### Opcao B - Extrair facades incrementalmente e eliminar `HardwareBridge` no final - -**Abordagem:** criar facades menores (`RenderSubmissionSink`, composer/logical frame facade, audio/input/assets), usar `HardwareBridge` apenas como scaffold temporario se necessario, migrar callers por etapas e remover `HardwareBridge` ao final. - -**Pros:** -- menor risco; -- permite commits pequenos; -- preserva testes e host desktop enquanto a fronteira e endurecida; -- facilita medir quando o worker real ja pode nascer. - -**Contras:** -- periodo intermediario com duas camadas; -- exige disciplina para nao continuar usando `gfx_mut()` no caminho errado. -- requer criterio explicito de remocao para evitar que o scaffold vire legado. - -**Manutenibilidade:** melhor equilibrio para o estado atual do repo. - -### Opcao C - Criar apenas `RenderSubmissionSink` agora e adiar o resto - -**Abordagem:** extrair somente a publicacao de submissions e manter composer/gfx/audio/input/assets no `HardwareBridge` por enquanto. - -**Pros:** -- menor mudanca inicial; -- desbloqueia parte do worker path. - -**Contras:** -- ainda deixa syscalls e composer acoplados ao hardware grande; -- worker real ainda pode esbarrar no viewport cache/composer vivo; -- risco de adiar o problema central. - -**Manutenibilidade:** aceitavel como primeiro PR, insuficiente como preparacao completa. - -## Sugestao / Recomendacao - -Recomendo a **Opcao B**, em fases, com uma restricao forte: **`HardwareBridge` deve ser eliminado no estado final**. - -1. Criar `RenderSubmissionSink` com erro tipado minimo, mantendo implementacao local em `prometeu_drivers::Hardware`. -2. Trocar publication local para depender de `RenderSubmissionSink`, nao do `HardwareBridge` inteiro. -3. Remover writes imediatos de `gfx_mut()` nas syscalls `gfx2d`/`gfxui`; syscalls devem apenas bufferizar comandos. -4. Extrair uma facade de composer/logical frame closure para `bind_scene`, `set_camera`, `emit_sprite`, `close_game2d_packet`. -5. Migrar firmware, Hub, runtime, host desktop e testes para as facades novas. -6. Remover `HardwareBridge` e qualquer dependencia nova dele. -7. Atualizar fixtures/testes para dependerem das facades certas. - -O criterio tecnico: antes da agenda do worker real virar plano, nenhum caminho de render worker deve precisar de `&mut Hardware`, `&mut Gfx`, `FrameComposer` vivo ou estado mutavel da VM. - -O criterio arquitetural: antes desta agenda fechar, `HardwareBridge` deve estar definido como artefato a ser removido, nao como compatibilidade preservada. - -## Perguntas em Aberto - -- [x] `HardwareBridge` deve sobreviver como compatibilidade final? - - Resposta: nao. Ele deve ser completamente eliminado no destino. -- [x] Se houver transicao incremental, `HardwareBridge` deve herdar facades menores ou apenas expor accessors temporarios? - - Resposta: se existir durante a migracao, deve usar accessors/adapters temporarios explicitos. Nao deve herdar facades menores, para nao parecer o novo contrato arquitetural. -- [x] `RenderSubmissionSink` deve receber `&RenderSubmission` ou owned `RenderSubmission`? - - Resposta: owned `RenderSubmission`. O contrato deve nascer pronto para handoff/worker; o caminho local pode adaptar internamente quando necessario. -- [x] O erro tipado minimo entra ja nesta preparacao ou fica para a agenda do worker real? - - Resposta: entra ja nesta preparacao, em forma minima. A API nao deve nascer infalivel para ser quebrada logo na agenda do worker real. -- [x] Qual e a primeira syscall/teste que prova que `gfx2d` nao desenha mais imediatamente? - - Resposta: comecar por `gfx2d.clear` e `gfx2d.draw_text`. Os testes devem provar que a syscall apenas bufferiza comando e que pixels mudam somente apos fechamento/publicacao da submission. -- [x] Composer deve ficar no runtime/logical side ou continuar em `prometeu_drivers::Hardware` enquanto nao houver worker? - - Resposta: deve ficar no logical side/runtime service. A migracao pode ser faseada, mas `bind_scene`, `set_camera`, `emit_sprite` e `close_game2d_packet` sao preparacao de frame logico, nao backend fisico. -- [x] Como preservar testes simples que hoje usam `Hardware::new()`? - - Resposta: criar `TestPlatform` como fixture explicita para testes. Nao preservar `HardwareBridge` como compatibilidade. -- [x] Que nomes vamos usar: `Hardware`, `HostHardware`, `LocalHardware`, `RuntimeHardware`, `RenderDevice`, `RenderSubmissionSink`? - - Resposta: usar `Platform`/`RuntimePlatform` para o agregado de servicos, `TestPlatform` para fixtures de teste, `RenderSubmissionSink` para submissao, `RenderBackend` para raster/present, e `Game2DFrameComposer` para o composer logico de Game2D. Evitar `Hardware` como nome arquitetural central. -- [x] Onde os contratos da platform layer devem viver? - - Resposta: contratos/facades em `prometeu-hal`; implementacoes locais/test em `prometeu-drivers`; integracao e ownership operacional no `prometeu-system` e nos hosts. -- [x] `Game2DFrameComposer` pertence ao runtime/logical side ou ao backend de render? - - Resposta: pertence ao logical side. O contrato deve ficar separado do backend renderizavel. A implementacao inicial pode migrar em fases, mas nao deve continuar sendo tratada como parte do hardware fisico. -- [x] Qual e o criterio para remover `HardwareBridge`? - - Resposta: remover quando runtime, firmware, Hub, host desktop e testes tiverem migrado para facades explicitas. A remocao deve ser plano proprio ou criterio final de um plano de migracao, nao compatibilidade opcional. -- [x] Aceitamos a quebra intencional de render imediato em `gfx2d`/`gfxui`? - - Resposta: sim. Syscalls devem bufferizar comandos; pixels devem mudar somente no fechamento/publicacao da submission. Testes que dependem de desenho imediato devem ser atualizados para o novo contrato. - -## Criterio para Encerrar - -Esta agenda pode virar decision quando tivermos: - -- sequencia clara de refatoracao para preparar a fronteira; -- definicao das facades minimas; -- estrategia para remover render imediato das syscalls; -- decisao de remocao do `HardwareBridge` e estrategia de transicao, se houver; -- estrategia de testes/fixtures; -- criterio objetivo de pronto para iniciar `AGD-0043`. - -## Discussion - -- 2026-06-06: Agenda reescopada. A `DSC-0042` passa a ter duas agendas: esta para preparacao da fronteira host/hardware/render, e `AGD-0043` para o worker real. -- 2026-06-06: Direcao levantada: `Hardware` deve ser uma abstracao/contrato implementado pelo host, nao um objeto concreto owned pelo runtime que cruza a fronteira do worker. -- 2026-06-06: Avaliacao inicial de impacto: mudanca media/alta, mas fatiavel. O acoplamento principal esta em `HardwareBridge`, `HostContext`, syscalls de `dispatch`, firmware, Hub, host desktop e testes. -- 2026-06-06: Reformulacao de objetivo: a intencao inicial era expor `Hardware` como interface consumida pelo runtime e implementada pelo host. Com a evolucao do sistema, isso pode estar limitado demais. O novo objetivo e reformular o runtime para um padrao mais proximo de uma plataforma handheld: runtime/OS como dono de lifecycle, scheduling, recursos e contratos; host/board support package como implementacao de dispositivos; render/audio/input/assets expostos por facades separadas e testaveis, nao por uma unica abstracao monolitica de hardware. -- 2026-06-06: Direcao cravada: `HardwareBridge` nao deve ser preservado como compatibilidade. No estado final da reformulacao ele deve ser completamente eliminado, substituido por servicos/facades explicitos de platform layer. -- 2026-06-06: Perguntas abertas respondidas: transicao por adapters/accessors temporarios, `RenderSubmissionSink` owned, erro tipado minimo ja na preparacao, primeiros testes em `gfx2d.clear`/`gfx2d.draw_text`, composer no logical side, fixtures via `TestPlatform`, e nomes preferidos `Platform`/`RuntimePlatform`, `RenderSubmissionSink`, `RenderBackend`, `Game2DFrameComposer`. -- 2026-06-06: Escopo confirmado: render pode migrar primeiro, mas a migracao deve cobrir todos os dominios atualmente presos ao `HardwareBridge`. O objetivo final inclui a remocao completa do `HardwareBridge`, nao apenas reduzir seu uso no render. -- 2026-06-06: Pontos adicionais aceitos: facades em `prometeu-hal`, implementacoes local/test em `prometeu-drivers`, integracao em `prometeu-system`/hosts; `Game2DFrameComposer` no logical side; `HardwareBridge` removido apos migracao completa dos callers; e quebra intencional do desenho imediato das syscalls `gfx2d`/`gfxui`. - -## Resolution - -`AGD-0042` resolve que o destino arquitetural nao e uma interface monolitica `HardwareBridge` implementada pelo host. O destino e uma platform layer composta por servicos/facades explicitos, com contratos em `prometeu-hal`, implementacoes local/test em `prometeu-drivers`, e integracao operacional em `prometeu-system` e nos hosts. - -`HardwareBridge` pode existir apenas como scaffold temporario de migracao. Ele deve ser completamente eliminado ao final, sem compatibilidade permanente. A migracao pode comecar por render, mas deve cobrir todos os dominios atualmente presos ao bridge: render, composer/frame, audio, input/touch, assets/storage, clock/pacing e telemetry quando aplicavel. - -O primeiro corte deve preparar o caminho do render worker sem implementa-lo ainda: introduzir `RenderSubmissionSink` owned com erro tipado minimo, remover desenho imediato das syscalls `gfx2d`/`gfxui`, mover `Game2DFrameComposer` para o logical side/runtime service, e substituir fixtures baseadas em `Hardware::new()` por `TestPlatform`. - -Esta agenda estara pronta para decision quando a decision puder definir a sequencia de migracao e os criterios de remocao final do `HardwareBridge` sem reabrir o contrato de render worker da `DSC-0040`. diff --git a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md b/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md deleted file mode 100644 index dd671366..00000000 --- a/discussion/workflow/agendas/AGD-0043-real-render-worker-establishment.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -id: AGD-0043 -ticket: real-render-worker-establishment -title: Real Render Worker Establishment -status: accepted -created: 2026-06-06 -resolved: -decision: -tags: [runtime, renderer, worker, concurrency, host, hal, architecture] ---- - -## Contexto - -`DEC-0031` definiu o contrato arquitetural da fronteira VM/render. `AGD-0042` preparou a fronteira host/hardware/render para que `Hardware` deixe de ser um objeto concreto atravessando o worker e passe a ser contrato/facades implementadas pelo host. - -Esta agenda discute a implementacao do worker real propriamente dito, assumindo que a preparacao da fronteira ja foi resolvida antes da execucao. - -### Estado Atual Depois da `AGD-0042` - -A preparacao de fronteira foi executada pelos planos `PLN-0098` a `PLN-0109`: - -- `HardwareBridge` foi removido do codigo e deixou de ser contrato publico; -- `RuntimePlatform` passou a ser a fronteira runtime-facing para render, input, audio, assets e telemetry; -- `RenderSubmissionSink` aceita submissao owned; -- `Game2DFrameComposer` separa fechamento logico de frame do backend de render; -- syscalls `gfx2d.*` e `gfxui.*` bufferizam comandos e nao desenham imediatamente via `Gfx`; -- firmware, Hub, runtime e host desktop passam por servicos de plataforma; -- testes de runtime/firmware usam `TestPlatform` como fixture local; -- specs de GFX/portabilidade documentam que o worker/render consumer nao deve depender de `&mut Hardware`, `&mut Gfx`, `FrameComposer` vivo ou estado mutavel da VM. - -O estado atual ainda nao tem worker real. O `LocalRenderWorker` continua sendo o caminho local/cooperativo que consome `RenderSubmission` no mesmo processo. O proximo passo desta agenda e decidir onde vive o controller do worker real, qual handoff thread-safe ele usa, e qual backend renderizavel ele owns ou recebe. - -## Problema - -O `LocalRenderWorker` atual e cooperativo/local. Ele prova o roteamento de policy, mas nao prova: - -- thread/core real; -- ownership do backend renderizavel; -- present loop; -- stop token para current work; -- bounded shutdown/join; -- stale epoch durante rasterizacao; -- erro tipado de render/present; -- repeat real do ultimo frame valido; -- ausencia de bloqueio da VM sob atraso do render. - -## Pontos Criticos - -### 1. Ownership e lifecycle do worker - -Precisamos decidir quem cria, inicia, para e reinicia o worker: runtime controller, host/HAL, ou ambos em contrato dividido. - -### 2. Handoff real - -O single-slot latest-wins ja existe como abstracao local. O worker real precisa de uma implementacao thread-safe, sem fila crescente e sem bloquear a VM. - -### 3. Backend renderizavel - -O worker deve owns um backend minimo ou receber capability host-owned. Esse backend deve consumir `RenderSubmission`, resolver recursos read-only e produzir/presentar frame sem `&mut Hardware`. - -### 4. Present cadence - -Precisamos decidir se o worker roda a 60Hz, se o host event loop dirige present, ou se ha uma autoridade separada de display cadence. Repeated present deve virar comportamento real, nao apenas hook. - -### 5. Shutdown/current work - -Quando houver uma submission em rasterizacao, shutdown e owner transition precisam impedir present obsoleto e terminar em tempo bounded. - -### 6. Error model - -Precisamos trocar o modelo infalivel/panic containment por erro tipado para render/present/worker failure. - -### 7. Testes de concorrencia - -Precisamos provar o contrato sem depender de janela nativa nem sleeps frageis. - -## Opcoes - -### Opcao A - Worker thread no runtime com backend mockavel - -**Abordagem:** runtime cria um worker thread generico que recebe um backend implementando trait testavel. - -**Pros:** -- testes de contrato mais diretos; -- menor dependencia do host desktop. - -**Contras:** -- runtime passa a owns detalhes de thread; -- precisa cuidado para nao absorver politica de host/window. - -**Manutenibilidade:** boa se o backend for limpo; ruim se virar acoplamento de host dentro do runtime. - -### Opcao B - Worker thread no host desktop primeiro - -**Abordagem:** implementar o primeiro worker no host winit/pixels e adaptar o runtime a submit/policy. - -**Pros:** -- valida o caso real visual; -- encaixa com surface/present/winit. - -**Contras:** -- mais dificil testar sem janela; -- risco de contrato ficar host-specific; -- portabilidade fica menos comprovada. - -**Manutenibilidade:** boa para desktop, menos boa para hardware proprio. - -### Opcao C - Worker controller runtime + backend host/HAL - -**Abordagem:** criar um controller de worker que implementa handoff, stop token, epoch check e telemetry; o backend concreto vem de host/HAL e pode ser mockado em testes. - -**Pros:** -- separa contrato de backend; -- testavel sem janela; -- mapeia para thread desktop, outro core ou fallback; -- preserva `RenderManager` como coordenador. - -**Contras:** -- desenho inicial mais exigente; -- depende da preparacao da `AGD-0042`. - -**Manutenibilidade:** melhor opcao se queremos worker real sem amarrar a winit. - -## Sugestao / Recomendacao - -Recomendo a **Opcao C - Worker controller runtime + backend host/HAL**. - -O primeiro worker real deveria: - -- consumir um handoff thread-safe single-slot; -- ter stop token e shutdown bounded; -- checar ownership/epoch antes de present; -- expor erros tipados; -- repetir o ultimo frame valido em cadence definida; -- sincronizar telemetry sem alterar semantica da VM; -- ter backend fake/mocked para testes de concorrencia. - -O host desktop pode ser a primeira integracao concreta, mas nao deve ser o unico lugar onde o contrato e testado. - -## Perguntas em Aberto - -### 1. Onde vive o worker controller? - -**Pergunta:** O worker controller vive em `prometeu-system`, `prometeu-hal`, `prometeu-drivers`, ou host? - -**Sugestao:** colocar o controller contratual em `prometeu-system`, proximo de `RenderManager`, e manter traits pequenas em `prometeu-hal` quando forem superficie compartilhada. `prometeu-drivers` deve continuar oferecendo implementacoes locais/testaveis; host desktop deve integrar o controller, nao definir o contrato. - -**Motivo:** o `RenderManager` ja owns policy de AppMode, ownership/epoch, latest-wins e telemetry. Se o controller nascer apenas no host, a semantica de worker vira desktop-specific e fica mais dificil provar o contrato sem janela nativa. - -### 2. Qual handoff thread-safe usar? - -**Pergunta:** O handoff thread-safe sera `Mutex>`, canal bounded, atomic slot, ou estrutura propria? - -**Sugestao:** comecar com uma estrutura propria simples baseada em `Mutex> + Condvar`, encapsulada como single-slot latest-wins. O produtor substitui a pending submission sem bloquear em rasterizacao; o consumidor espera por nova submission ou shutdown. - -**Motivo:** canal bounded tende a preservar historico ou bloquear produtor se nao for usado com cuidado. Um slot explicito modela diretamente a regra ja aceita: nao existe fila crescente, a ultima submissao completa vence. - -### 3. Como modelar current work, shutdown e stale owner? - -**Pergunta:** Como modelar current work para shutdown e stale owner? - -**Sugestao:** separar `pending` de `in_flight`. Ao tomar uma submission, o worker carimba `in_flight` localmente, renderiza, e checa ownership/epoch contra um snapshot atomico ou handle compartilhado antes de present. Shutdown sinaliza stop, acorda o worker e faz join bounded; se houver trabalho em andamento, ele pode terminar rasterizacao, mas nao pode apresentar se o owner/epoch estiver obsoleto ou o stop ja exigir descarte. O timeout deve ser configuravel; como default inicial, usar `250ms`, que e longo o bastante para evitar falso positivo em desktop comum e curto o bastante para detectar worker preso sem travar encerramento. - -**Motivo:** isso evita que shutdown dependa de cancelar mid-raster em codigo que talvez nao seja cancelavel, mas ainda impede present obsoleto. - -### 4. Quem chama present? - -**Pergunta:** Quem chama present: worker, host event loop, ou display cadence service? - -**Sugestao:** no primeiro corte, o worker deve produzir/publish uma superficie pronta como buffer RGBA8888 owned, por exemplo `OwnedRgba8888Frame`, definido em `prometeu-hal`. O host event loop continua sendo autoridade de apresentacao da janela nativa. O "repeat ultimo frame valido" deve repetir o ultimo buffer publicado, nao recompor nem exigir que o worker conheca a API nativa de janela/swap. - -**Motivo:** RGBA8888 ja e o formato logico canonico nas specs. Um frame owned com `FrameId`, ownership/epoch, largura, altura, `stride_pixels` e pixels RGBA8888 em `Vec` deixa o contrato testavel sem janela e evita acoplamento a winit/pixels, SDL, swapchain ou textura nativa. Colocar o swap real dentro do worker pode funcionar em alguns hosts e quebrar portabilidade. O contrato importante e que a VM nao bloqueie e que o host tenha uma superficie publicada coerente. - -### 5. Como o worker recebe recursos read-only? - -**Pergunta:** Como o worker recebe recursos read-only preparados na `AGD-0042`? - -**Sugestao:** o primeiro backend deve receber `RenderSubmission` owned e uma interface compacta de acesso read-only aos banks por id. Banks sao read-only por definicao durante o consumo de render; o worker resolve recursos por ids estaveis, sem copiar bank, sem snapshot de bank e sem depender de `Arc` como requisito do contrato. - -**Motivo:** `AGD-0042` eliminou o bridge monolitico exatamente para impedir que o worker dependa do hardware inteiro. A proxima decisao precisa declarar quais recursos sao lidos pelo render backend e como sua estabilidade e garantida durante uma submissao em voo. A estabilidade vem da regra de leitura: o worker ve banks por id via interface read-only, enquanto instalacao/mutacao de bank permanece fora do consumo renderizavel em voo. - -### 6. Qual erro tipado minimo? - -**Pergunta:** Qual erro tipado minimo precisamos no primeiro worker? - -**Sugestao:** introduzir um erro pequeno, por exemplo `RenderWorkerError`, cobrindo pelo menos: backend unavailable, render failed, present/publish failed, shutdown timeout, stale submission discarded. Panics continuam sendo capturados como falha interna, mas nao devem ser o modelo operacional normal. - -**Motivo:** `LocalRenderWorker` hoje prova policy, mas nao diferencia falhas reais de render/present. O worker real precisa reportar falhas sem contaminar a VM com detalhes de host. - -### 7. Como provar que a VM nao bloqueia? - -**Pergunta:** Como provar que a VM nao bloqueia quando o worker atrasa? - -**Sugestao:** criar testes deterministas com backend fake controlado por barreiras/condvars, nao sleeps. O teste deve segurar o worker em rasterizacao, produzir frames adicionais no runtime, verificar substituicao latest-wins/drop telemetry, e provar que o tick da VM retorna sem esperar o consumo terminar. - -**Motivo:** sleeps tornam teste de concorrencia fragil. Barreiras permitem provar especificamente a propriedade desejada: produtor nao bloqueia em raster/present lento. - -### 8. Qual primeiro backend real? - -**Pergunta:** Qual sera o primeiro backend real: desktop `Gfx`/pixels, framebuffer local, ou fake backend? - -**Sugestao:** implementar primeiro um backend fake/mocked para contrato e um backend local framebuffer para integracao sem janela. A integracao desktop vem em seguida como consumidor concreto, usando a mesma trait. Ao final desta agenda, o worker deve estar funcionando completamente no caminho host real; a divisao em planos serve para controlar risco, nao para reduzir o alvo final. - -**Motivo:** se o primeiro backend for desktop/winit, o contrato corre risco de nascer acoplado a detalhes de janela. Se o primeiro for fake/local, a semantica do worker fica testavel antes da integracao visual. Ainda assim, a agenda so deve ser considerada concluida quando a integracao host real tambem estiver operando sobre o worker. - -## Criterio para Encerrar - -Esta agenda pode virar decision quando tivermos: - -- local/camada do worker controller; -- estrutura thread-safe de handoff; -- trait/backend de render/present; -- present cadence/repeat; -- shutdown/current work; -- error model; -- test matrix de concorrencia; -- escopo do primeiro worker real. - -## Discussion - -- 2026-06-06: Agenda criada dentro da `DSC-0042` para separar worker real da preparacao de fronteira discutida em `AGD-0042`. -- 2026-06-15: Apos a conclusao dos planos `PLN-0098` a `PLN-0109`, a direcao da agenda foi alinhada em torno da Opcao C: controller em `prometeu-system`, handoff single-slot thread-safe, backend fake/local primeiro, e publicacao de frame owned RGBA8888 antes da integracao desktop concreta. -- 2026-06-15: Perguntas restantes alinhadas: `OwnedRgba8888Frame` deve viver em `prometeu-hal` com pixels `Vec` RGBA8888; banks sao acessados por interface compacta read-only via ids, sem copia/snapshot/`Arc` como contrato; shutdown deve ter timeout configuravel com default inicial sugerido de `250ms`; a agenda pode virar varios plans, mas so fecha quando o worker funcionar no host real. - -## Resolution - -Direcao proposta para decisao: - -- `RenderManager` continua como autoridade de policy, ownership/epoch, latest-wins e telemetry; -- o worker controller contratual deve viver em `prometeu-system`; -- traits compartilhadas minimas podem viver em `prometeu-hal`; -- o handoff real deve ser single-slot latest-wins com sincronizacao explicita, inicialmente `Mutex> + Condvar`; -- o worker separa `pending` de `in_flight` e checa stop/ownership/epoch antes de publicar; -- shutdown deve ser bounded e configuravel; default inicial sugerido: `250ms`; -- o worker publica um `OwnedRgba8888Frame` em `prometeu-hal`, contendo frame id, owner/epoch, dimensoes, `stride_pixels` e pixels RGBA8888 owned em `Vec`; -- o worker acessa banks por ids estaveis atraves de uma interface compacta read-only; nao copia banks, nao depende de snapshots de bank e nao usa `Arc` como parte obrigatoria do contrato; -- o host event loop faz o upload/present nativo a partir do ultimo frame publicado; -- o primeiro backend deve ser fake/local para provar contrato e concorrencia sem janela; -- a integracao desktop concreta vem depois, usando a mesma trait/backend contract; -- a agenda pode ser executada em varios plans, mas o criterio final e ter o worker funcionando completamente no host real; -- testes devem usar barreiras/condvars para provar ausencia de bloqueio da VM, latest-wins, stale discard, shutdown bounded e telemetry. diff --git a/discussion/workflow/decisions/DEC-0032-platform-layer-and-hardwarebridge-elimination.md b/discussion/workflow/decisions/DEC-0032-platform-layer-and-hardwarebridge-elimination.md deleted file mode 100644 index 33e2abaf..00000000 --- a/discussion/workflow/decisions/DEC-0032-platform-layer-and-hardwarebridge-elimination.md +++ /dev/null @@ -1,198 +0,0 @@ ---- -id: DEC-0032 -ticket: real-render-worker-establishment -title: Platform Layer and HardwareBridge Elimination -status: accepted -created: 2026-06-06 -accepted: -agenda: AGD-0042 -plans: [PLN-0098, PLN-0099, PLN-0100, PLN-0101, PLN-0102, PLN-0103, PLN-0104, PLN-0105, PLN-0106, PLN-0107, PLN-0108, PLN-0109] -tags: [runtime, platform, hardware, host, hal, renderer, architecture] ---- - -## Status - -Decision draft emitted from accepted agenda `AGD-0042`. - -This decision is ready for review. Once accepted, it becomes the normative contract for plans that prepare Prometeu's platform layer before the real render worker work in `AGD-0043`. - -## Contexto - -`DEC-0031` established the VM/render boundary: closed render submissions, single-slot handoff, frame pacing, AppMode policy, ownership epoch, and render telemetry. - -The current implementation still depends on `HardwareBridge`, a monolithic trait that exposes render, composer, `Gfx`, audio, input, touch, assets, frame begin, and presentation through one runtime-facing object. That bridge was useful as an early abstraction, but it is no longer sufficient for a high-quality handheld-style runtime. - -The goal is no longer to make `HardwareBridge` the permanent host-implemented interface. The goal is to replace it with an explicit platform layer made of domain services/facades. Runtime code should depend on stable platform contracts; host/board code should provide concrete implementations. - -This decision prepares the platform boundary. It does not implement the real render worker. Worker establishment remains scoped to `AGD-0043`. - -## Decisao - -Prometeu SHALL replace the monolithic `HardwareBridge` model with a platform layer composed of explicit domain services/facades. - -`HardwareBridge` SHALL NOT survive as a final compatibility contract. It MAY exist only as temporary migration scaffold. The end state MUST remove `HardwareBridge` and all production/runtime dependencies on it. - -The migration MAY begin with render, because render blocks the real worker path, but it MUST NOT stop there. The migration MUST cover every domain currently coupled through `HardwareBridge`: render, Game2D frame composition, audio, input/touch, assets/storage, clock/pacing, and telemetry where applicable. - -The first implementation wave SHALL prepare render-worker-safe boundaries without introducing the real worker yet: - -- introduce an owned `RenderSubmissionSink`; -- introduce minimal typed render submission errors; -- remove immediate framebuffer writes from `gfx2d` and `gfxui` syscalls; -- move Game2D frame composition into a logical-side `Game2DFrameComposer` service/facade; -- replace test fixtures centered on `Hardware::new()` with `TestPlatform`. - -## Rationale - -A handheld-quality runtime needs clear separation between platform policy and board/host implementation. - -The runtime/OS owns: - -- lifecycle and AppMode policy; -- VM execution; -- frame pacing; -- render ownership and handoff; -- logical frame composition; -- command buffers; -- resource visibility policy; -- telemetry semantics. - -The host/board implementation owns: - -- window/surface or physical framebuffer; -- concrete present implementation; -- physical input events; -- audio backend; -- IO/storage backend; -- host threads, cores, DMA, or coprocessor mapping. - -A single `HardwareBridge` encourages accidental coupling. It lets VM syscalls, firmware, Hub, renderer, audio, input, assets, and present share the same mutable object. That model makes it too easy for future worker work to pass `&mut Hardware`, `&mut Gfx`, or live `FrameComposer` state across the render boundary. - -Explicit facades make dependencies visible and testable. They also let each domain evolve with the lifecycle, threading, and error semantics it actually needs. - -## Invariantes / Contrato - -### 1. No final `HardwareBridge` - -`HardwareBridge` MUST be removed from the final architecture of this migration. - -During migration, code MAY use adapters or temporary accessors to bridge old and new APIs. Those adapters MUST be treated as implementation scaffolding, not as new public/runtime architecture. - -`HardwareBridge` MUST NOT inherit the new facades as a way to preserve itself as a permanent aggregate contract. - -### 2. Facades live at the right layer - -Contracts/facades SHALL live in `prometeu-hal`. - -Local/default and test implementations SHALL live in `prometeu-drivers` unless a domain has a stronger existing home. - -Operational integration and ownership orchestration SHALL live in `prometeu-system` and host crates. - -### 3. Render submission is owned - -`RenderSubmissionSink` SHALL accept owned `RenderSubmission` values. - -The owned contract is required because asynchronous handoff transfers ownership to a consumer. Local/synchronous paths MAY adapt internally, but the facade MUST be compatible with future worker handoff. - -### 4. Render submission errors are typed - -The preparation work SHALL introduce a minimal typed error surface for submission/render publication. - -The API MUST NOT be born infallible if the next worker stage will immediately need typed failure. Panic containment may remain as a safety net, but normal backend failure should have an explicit result type. - -### 5. `gfx2d` and `gfxui` syscalls buffer commands only - -`gfx2d.*` and `gfxui.*` syscalls MUST NOT mutate the framebuffer immediately. - -Those syscalls SHALL record commands into the appropriate logical command buffer. Pixels change only when a closed render submission is consumed/published by the render path. - -The first regression tests MUST include `gfx2d.clear` and `gfx2d.draw_text` proving that syscalls buffer commands and presentation happens only after frame closure/publication. - -### 6. Game2D composition is logical-side state - -`bind_scene`, `set_camera`, `emit_sprite`, and `close_game2d_packet` are logical frame preparation, not physical backend behavior. - -The migration SHALL introduce `Game2DFrameComposer` as the logical-side service/facade for Game2D frame composition. Its implementation may move in phases, but it MUST NOT remain conceptually part of a physical render backend. - -### 7. Tests use `TestPlatform` - -Tests that need a full local platform fixture SHALL migrate toward `TestPlatform`. - -`TestPlatform` SHOULD keep tests ergonomic while making dependencies explicit through the new facades. It MUST NOT preserve `HardwareBridge` as hidden compatibility. - -### 8. Render may migrate first, but the migration is complete only when every domain is moved - -The render path MAY be migrated first. - -The architecture is not complete until audio, input/touch, assets/storage, clock/pacing, telemetry where applicable, firmware, Hub, runtime, host desktop, and tests no longer depend on `HardwareBridge`. - -## Impactos - -### Spec - -Specs need to describe the platform layer as explicit services/facades rather than a monolithic hardware bridge. - -Render specifications must state that `gfx2d`/`gfxui` syscalls buffer commands and that framebuffer mutation occurs through render submission consumption/publication. - -### Runtime - -Runtime code must stop depending on `&mut dyn HardwareBridge`. - -VM dispatch must stop using `hw.gfx_mut()` for immediate drawing. Runtime frame closure must depend on logical services such as `Game2DFrameComposer` and publication facades such as `RenderSubmissionSink`. - -### HAL - -`prometeu-hal` must define the stable contracts for the platform layer. - -At minimum, the first wave needs `RenderSubmissionSink` and its typed error. Later waves should define or refine composer, audio, input/touch, assets/storage, clock/pacing, and telemetry facades. - -### Drivers - -`prometeu-drivers` should provide local/default implementations and `TestPlatform`. - -The existing `prometeu_drivers::Hardware` may be used as a migration source, but it must not remain the final runtime-facing architecture. - -### Host - -Host crates must move from directly owning and exposing `Hardware` as the runtime's universal bridge toward implementing platform services. - -The desktop host may remain the first concrete integration, but the contracts must remain portable to handheld board support, emulator, web, or coprocessor/DMA implementations. - -### Firmware / Hub - -Firmware and Hub must migrate away from `publish_render_submission` on `HardwareBridge` and toward explicit render submission publication/platform services. - -### Tests - -Tests must migrate from direct `Hardware::new()` dependence to `TestPlatform` or narrower facade-specific fixtures. - -Tests must be updated where they assume immediate framebuffer mutation from `gfx2d`/`gfxui` syscalls. - -## Referencias - -- `AGD-0042`: Host Hardware and Render Boundary Preparation. -- `AGD-0043`: Real Render Worker Establishment. -- `DEC-0031`: VM and Render Parallel Execution Boundary. -- `LSN-0048`: Render Workers Need a Closed Packet Contract. - -## Propagacao Necessaria - -Create implementation plans before editing specs or code. - -Recommended plan sequence: - -1. introduce `RenderSubmissionSink` and minimal typed submit error; -2. migrate local render publication away from `HardwareBridge`; -3. remove immediate framebuffer writes from `gfx2d`/`gfxui` syscalls and update tests; -4. introduce `Game2DFrameComposer` as logical-side service/facade; -5. introduce `RuntimePlatform`/`Platform` and `TestPlatform`; -6. migrate firmware, Hub, runtime, host desktop, and tests to explicit facades; -7. migrate remaining domains: audio, input/touch, assets/storage, clock/pacing, telemetry as needed; -8. remove `HardwareBridge`; -9. update specs and lessons after implementation publishes the new structure. - -`AGD-0043` SHOULD NOT move to implementation until this decision has produced enough platform boundary work that a render worker can be built without `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state crossing into the worker. - -## Revision Log - -- 2026-06-06: Initial decision draft generated from accepted `AGD-0042`. diff --git a/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md b/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md deleted file mode 100644 index 9d24b9f4..00000000 --- a/discussion/workflow/decisions/DEC-0033-real-render-worker-contract.md +++ /dev/null @@ -1,296 +0,0 @@ ---- -id: DEC-0033 -ticket: real-render-worker-establishment -title: Real Render Worker Contract -status: accepted -created: 2026-06-15 -accepted: -agenda: AGD-0043 -plans: [PLN-0110, PLN-0111, PLN-0112, PLN-0113, PLN-0114, PLN-0115, PLN-0116, PLN-0117, PLN-0118, PLN-0119, PLN-0120, PLN-0121] -tags: [runtime, renderer, worker, concurrency, host, hal, architecture] ---- - -## Status - -Decision accepted from agenda `AGD-0043`. - -This is the normative contract for implementation plans that establish the real render worker for Prometeu. - -## Contexto - -`DEC-0031` established the VM/render boundary around closed render submissions, single-slot handoff, frame pacing, AppMode policy, ownership epoch, and render telemetry. - -`DEC-0032` and plans `PLN-0098` through `PLN-0109` prepared the platform boundary required for a real worker: - -- `HardwareBridge` was removed from production/runtime contracts; -- `RuntimePlatform` became the runtime-facing platform service boundary; -- `RenderSubmissionSink` accepts owned render submissions; -- `Game2DFrameComposer` separates logical Game 2D frame closure from backend rendering; -- `gfx2d.*` and `gfxui.*` syscalls buffer commands instead of mutating `Gfx` immediately; -- firmware, Hub, runtime, host desktop, and tests moved to platform services; -- specs now state that worker/render consumers must not depend on `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state. - -The remaining work is to replace the cooperative/local `LocalRenderWorker` proof with a real worker that drains render submissions without blocking VM progress and publishes complete RGBA8888 frames for host presentation. - -## Decisao - -Prometeu SHALL implement the real render worker as a runtime-owned worker controller with host-provided backend integration. - -The worker controller SHALL live in `prometeu-system`, near `RenderManager`, because `RenderManager` owns AppMode policy, ownership/epoch, latest-wins handoff semantics, and render telemetry. - -Shared contract types and traits SHALL live in `prometeu-hal` when they cross runtime/host/driver boundaries. Local/default and test implementations SHALL live in `prometeu-drivers` or host crates as appropriate. - -The worker SHALL consume owned `RenderSubmission` values through a thread-safe single-slot latest-wins handoff. The VM producer MUST NOT block on rasterization, publication, presentation, or worker completion. - -The worker SHALL publish owned RGBA8888 frames, not native window/swapchain objects. The initial shared frame type SHALL be `OwnedRgba8888Frame` in `prometeu-hal`. - -The host event loop SHALL remain responsible for native window upload/present. The worker MUST NOT depend on winit, pixels, SDL, swapchain handles, native textures, or host window APIs. - -The agenda may be implemented through multiple plans, but `AGD-0043` is complete only when the worker functions on the real host path, not merely in fake/local tests. - -## Rationale - -The real worker must prove the architecture promised by `DEC-0031`: VM execution and render consumption can proceed independently without crossing mutable runtime state into the renderer. - -Putting the controller in `prometeu-system` keeps render policy in the same layer that already owns frame lifecycle, ownership epochs, latest-wins behavior, and telemetry. Putting it only in the host would make the contract desktop-specific and harder to test without a window. - -Publishing an owned RGBA8888 frame keeps the worker host-agnostic. The host can upload that frame to any native presentation backend while the worker stays testable through ordinary memory comparisons. - -Using a single explicit pending slot directly models the accepted backpressure rule. Channels or queues can accidentally preserve history or block producers. The runtime contract is not "render every produced frame"; it is "the latest complete submission wins and the VM does not wait for the consumer." - -Read-only bank access by id preserves the no-copy resource boundary. Banks are render resources, not payloads to duplicate into each submission. The worker should resolve resource ids through a compact read-only interface without taking `&mut Hardware`, `&mut Gfx`, a live `FrameComposer`, or VM state. - -## Invariantes / Contrato - -### 1. Runtime controller ownership - -The worker controller MUST live in `prometeu-system`. - -`RenderManager` SHALL remain the authority for: - -- AppMode render policy; -- ownership/epoch; -- latest-wins handoff semantics; -- render telemetry; -- stale submission discard; -- shutdown/request-stop coordination visible to runtime policy. - -Host crates MAY instantiate or wire the controller, but they MUST NOT redefine worker semantics. - -### 2. Shared HAL contracts - -`prometeu-hal` SHALL define shared types/traits that cross the runtime/host/driver boundary. - -At minimum, the worker work SHALL introduce: - -- `OwnedRgba8888Frame`; -- compact read-only render resource access contracts for bank lookup by id; -- typed worker/backend error contracts where they are shared across crates. - -### 3. Owned RGBA8888 frame publication - -`OwnedRgba8888Frame` SHALL represent a completed logical frame ready for host upload/present. - -It MUST include: - -- `FrameId`; -- render ownership/epoch metadata; -- width; -- height; -- `stride_pixels`; -- owned RGBA8888 pixel data as `Vec`. - -The pixel data MUST use the canonical RGBA8888 raw value contract already used by the runtime specs. It MUST NOT be described as native-endian host pixels, native texture data, or backend-specific surface memory. - -Repeating the last valid frame SHALL mean reusing the last published `OwnedRgba8888Frame`, not recomposing from VM state or live composer state. - -### 4. Thread-safe latest-wins handoff - -The real handoff SHALL be a thread-safe single-slot latest-wins structure. - -The initial implementation SHOULD use an explicit slot based on `Mutex> + Condvar`. - -Producer behavior: - -- publish replaces the pending submission if one already exists; -- replacement increments drop/replacement telemetry; -- publish MUST NOT wait for rasterization or present; -- publish MUST wake the consumer when needed. - -Consumer behavior: - -- wait for pending submission or shutdown; -- take ownership of the latest pending submission; -- leave the pending slot empty while work is in flight; -- discard stale work before publication if ownership/epoch no longer matches. - -### 5. Pending and in-flight are separate - -The worker MUST distinguish pending submissions from in-flight work. - -Once the worker takes a submission, that submission is in flight. New producer submissions may replace the pending slot while the worker is still rasterizing the previous in-flight submission. - -Before publishing an in-flight frame, the worker MUST check: - -- stop/shutdown state; -- render owner; -- render epoch/generation. - -If the frame is stale, the worker MUST discard it and update telemetry. It MUST NOT publish stale pixels. - -### 6. Shutdown is bounded - -Worker shutdown MUST be bounded and observable. - -The shutdown timeout MUST be configurable. The initial default SHOULD be `250ms`. - -On shutdown: - -- stop state MUST be signaled; -- the worker MUST be woken if it is waiting; -- join MUST complete within the configured bound or return/report a `ShutdownTimeout` failure; -- in-flight work MAY finish rasterization, but MUST NOT publish after stop/epoch invalidation says it is stale. - -### 7. Read-only bank access by id - -The render backend SHALL resolve render resources through a compact read-only interface by stable ids. - -Banks are read-only by definition during render consumption. The worker MUST NOT copy entire banks into submissions. The worker MUST NOT require bank snapshots. The worker MUST NOT require `Arc` as part of the public contract. - -Implementation may use any internal storage strategy that satisfies the public contract, but the render worker contract is ids plus read-only access. - -Resource installation, mutation, and residency changes remain outside in-flight render consumption. - -### 8. Backend and host presentation split - -The worker backend SHALL consume `RenderSubmission` plus read-only resource access and produce `OwnedRgba8888Frame`. - -The host event loop SHALL upload/present the latest published frame through its native API. - -The worker MUST NOT call native window present directly unless a future decision revises the host presentation model. - -### 9. Typed error model - -The worker work SHALL introduce a minimal typed error model, such as `RenderWorkerError`. - -The error model MUST cover at least: - -- backend unavailable; -- render failed; -- publish/present handoff failed; -- shutdown timeout; -- stale submission discarded; -- worker panic captured as internal failure. - -Panic containment MAY remain as a safety net. Ordinary worker/backend failure MUST NOT rely on panic as the normal reporting path. - -### 10. Telemetry - -Worker telemetry SHALL preserve and extend the existing render telemetry model. - -At minimum, telemetry SHOULD include: - -- submissions produced; -- pending submissions replaced/dropped; -- submissions consumed; -- frames published/presented; -- stale submissions discarded; -- render failures; -- repeated-frame count; -- shutdown timeout count. - -Telemetry MUST NOT change VM semantics. - -### 11. Test contract - -The worker contract MUST be proven without a native window. - -Tests SHALL use deterministic synchronization primitives such as barriers and condvars, not timing sleeps as the primary proof mechanism. - -The test matrix MUST prove: - -- VM producer does not block when worker rasterization is held; -- latest-wins replacement occurs while a frame is in flight; -- stale owner/epoch prevents publication; -- shutdown is bounded; -- typed errors and telemetry are emitted; -- repeat uses the last published `OwnedRgba8888Frame`; -- fake/local backend behavior matches the contract before desktop integration. - -### 12. Final scope - -The implementation MAY be split into multiple plans. - -However, the agenda is not complete until the real host path uses the worker. A fake/local backend is required for contract tests, but it is not sufficient as the final state of `AGD-0043`. - -## Impactos - -### Spec - -Runtime specs must document: - -- `OwnedRgba8888Frame`; -- the thread-safe single-slot latest-wins worker handoff; -- the worker/host presentation split; -- read-only bank access by id; -- shutdown and typed error behavior; -- telemetry expectations. - -### Runtime - -`prometeu-system` must add the worker controller and integrate it with `RenderManager`. - -The VM tick path must submit render work without blocking on worker consumption. - -### HAL - -`prometeu-hal` must expose shared worker contracts: - -- owned RGBA8888 frame type; -- render resource read-only access traits; -- worker/backend error types as needed. - -### Drivers - -`prometeu-drivers` must provide fake/local implementations suitable for deterministic tests and non-window integration. - -Concrete local rendering may continue to use existing GFX machinery internally, but must expose the worker-facing contract without leaking `&mut Hardware`, `&mut Gfx`, or live `FrameComposer`. - -### Host - -The desktop host must integrate the real worker path and upload/present the latest published `OwnedRgba8888Frame` through its native window backend. - -The host owns native presentation; the worker owns render consumption and frame publication. - -### Tests - -Tests must cover concurrency behavior deterministically and must include final host-path integration evidence. - -## Referencias - -- `AGD-0043`: Real Render Worker Establishment. -- `AGD-0042`: Host Hardware and Render Boundary Preparation. -- `DEC-0031`: VM and Render Parallel Execution Boundary. -- `DEC-0032`: Platform Layer and HardwareBridge Elimination. -- `LSN-0048`: Render Workers Need a Closed Packet Contract. -- `docs/specs/runtime/04-gfx-peripheral.md`. -- `docs/specs/runtime/11-portability-and-cross-platform-execution.md`. - -## Propagacao Necessaria - -Create implementation plans before editing specs or code. - -Recommended plan sequence: - -1. define `OwnedRgba8888Frame`, read-only bank access contracts, and worker error types in HAL; -2. introduce the thread-safe latest-wins handoff and deterministic fake/local worker tests; -3. implement worker controller lifecycle, stop token, bounded shutdown, pending/in-flight separation, and telemetry in `prometeu-system`; -4. implement fake/local backend that consumes `RenderSubmission` and produces `OwnedRgba8888Frame`; -5. integrate runtime render submission path with the worker without blocking VM ticks; -6. add stale owner/epoch discard and repeat-last-published-frame behavior; -7. integrate desktop host upload/present from the latest published `OwnedRgba8888Frame`; -8. update specs and final validation tests. - -## Revision Log - -- 2026-06-15: Initial decision draft generated from accepted `AGD-0043`. diff --git a/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md b/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md deleted file mode 100644 index f29b2154..00000000 --- a/discussion/workflow/plans/PLN-0098-define-platform-facade-contracts.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -id: PLN-0098 -ticket: real-render-worker-establishment -title: Define Platform Facade Contracts -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [runtime, platform, hal, facades] ---- - -## Objective - -Define the first `prometeu-hal` platform facade contracts that will replace the monolithic `HardwareBridge` model. - -## Background - -`DEC-0032` requires Prometeu to move from one runtime-facing `HardwareBridge` to explicit platform services/facades. This first plan creates the stable contract surface without migrating callers yet. - -## Scope - -### Included - -- Add facade modules/types in `crates/console/prometeu-hal`. -- Define names and module ownership for `RuntimePlatform`, `RenderSubmissionSink`, `RenderBackend`, `Game2DFrameComposer`, and `TestPlatform` integration points. -- Add minimal documentation explaining that `HardwareBridge` is legacy migration scaffold. - -### Excluded - -- Do not remove `HardwareBridge`. -- Do not migrate syscalls, firmware, Hub, host, or tests yet. -- Do not implement the real render worker. - -## Execution Steps - -### Step 1 - Add platform facade module - -**What:** Create a `platform` module in `prometeu-hal`. -**How:** Add facade trait declarations and re-exports without changing existing call sites. -**File(s):** `crates/console/prometeu-hal/src/platform.rs`, `crates/console/prometeu-hal/src/lib.rs`. - -### Step 2 - Declare domain facade boundaries - -**What:** Define initial placeholder traits for render submission, render backend, Game2D composer, audio, input/touch, assets/storage, clock/pacing, and telemetry. -**How:** Keep non-render traits minimal or marker-like when detailed APIs are not yet migrated. -**File(s):** `crates/console/prometeu-hal/src/platform.rs`. - -### Step 3 - Document migration status - -**What:** Mark `HardwareBridge` as legacy scaffold. -**How:** Add module-level comments stating it must be removed by later plans. -**File(s):** `crates/console/prometeu-hal/src/hardware_bridge.rs`. - -## Test Requirements - -### Unit Tests - -- Compile-only tests or trait object assertions where useful. - -### Integration Tests - -- Existing HAL/system/driver tests must continue to pass. - -### Manual Verification - -- Confirm `HardwareBridge` has no new dependencies on the platform facades. - -## Acceptance Criteria - -- [ ] Platform facade module exists in `prometeu-hal`. -- [ ] `HardwareBridge` is explicitly documented as migration scaffold. -- [ ] No runtime behavior changes. -- [ ] Existing tests pass. - -## Dependencies - -- `DEC-0032`. - -## Risks - -- Introducing too much API too early can freeze poor names. Keep this plan narrow. diff --git a/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md b/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md deleted file mode 100644 index 17651383..00000000 --- a/discussion/workflow/plans/PLN-0099-introduce-owned-render-submission-sink.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -id: PLN-0099 -ticket: real-render-worker-establishment -title: Introduce Owned RenderSubmissionSink -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [runtime, renderer, hal, errors] ---- - -## Objective - -Introduce an owned `RenderSubmissionSink` facade with minimal typed submission errors. - -## Background - -`DEC-0032` requires render publication to move away from `HardwareBridge` and to accept owned `RenderSubmission` values so the contract is compatible with async handoff. - -## Scope - -### Included - -- Define `RenderSubmissionSink`. -- Define minimal `RenderSubmitError`. -- Implement the facade for the current local driver path. - -### Excluded - -- Do not migrate all publication call sites yet. -- Do not spawn a render worker. -- Do not remove `HardwareBridge::publish_render_submission`. - -## Execution Steps - -### Step 1 - Define sink and error - -**What:** Add `RenderSubmissionSink` and `RenderSubmitError`. -**How:** `submit_render_submission(&mut self, submission: RenderSubmission) -> Result<(), RenderSubmitError>`. -**File(s):** `crates/console/prometeu-hal/src/platform.rs` or a render-specific HAL module. - -### Step 2 - Implement local sink - -**What:** Implement the sink for the current local driver implementation. -**How:** Adapt owned submission to the existing local `Hardware` publication path. -**File(s):** `crates/console/prometeu-drivers/src/hardware.rs`. - -### Step 3 - Add tests - -**What:** Prove owned submission is consumed by the local sink. -**How:** Add driver test using `RenderSubmission::game2d` and `RenderSubmission::shell_ui`. -**File(s):** `crates/console/prometeu-drivers/src/hardware.rs` or platform test module. - -## Test Requirements - -### Unit Tests - -- Local sink accepts owned Game2D and ShellUi submissions. -- Error type is constructible and debuggable. - -### Integration Tests - -- `cargo test -p prometeu-hal -p prometeu-drivers`. - -### Manual Verification - -- Confirm the sink does not take `&RenderSubmission`. - -## Acceptance Criteria - -- [ ] `RenderSubmissionSink` takes owned `RenderSubmission`. -- [ ] Minimal typed error exists. -- [ ] Local driver implements the sink. -- [ ] Existing publication behavior remains intact. - -## Dependencies - -- `PLN-0098`. - -## Risks - -- Local paths may still need temporary borrow adaptation. Keep it internal to the implementation. diff --git a/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md b/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md deleted file mode 100644 index 550b66a6..00000000 --- a/discussion/workflow/plans/PLN-0100-migrate-local-render-publication-to-sink.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -id: PLN-0100 -ticket: real-render-worker-establishment -title: Migrate Local Render Publication to RenderSubmissionSink -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [runtime, renderer, platform] ---- - -## Objective - -Move local render publication call sites from `HardwareBridge::publish_render_submission` to `RenderSubmissionSink`. - -## Background - -The sink must become the publication boundary before a real worker can replace the local path. Runtime, firmware, and Hub currently publish through the monolithic bridge. - -## Scope - -### Included - -- Runtime local render surface uses `RenderSubmissionSink`. -- Firmware splash/crash publication uses the sink. -- Hub Shell UI publication uses the sink or a temporary adapter. - -### Excluded - -- Do not remove `HardwareBridge`. -- Do not migrate non-render domains. -- Do not change command buffering semantics yet. - -## Execution Steps - -### Step 1 - Adapt runtime tick publication - -**What:** Replace `HardwareRenderSurface` dependency on `HardwareBridge::publish_render_submission`. -**How:** Route submission through `RenderSubmissionSink`. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tick.rs`. - -### Step 2 - Adapt firmware and Hub publication - -**What:** Stop direct calls to `hw.publish_render_submission`. -**How:** Use the sink through explicit platform/render facade access or temporary adapter. -**File(s):** `crates/console/prometeu-firmware/src/firmware_step_splash_screen.rs`, `crates/console/prometeu-firmware/src/firmware_step_crash_screen.rs`, `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`. - -### Step 3 - Update tests - -**What:** Adjust tests that call `HardwareBridge::publish_render_submission`. -**How:** Use `RenderSubmissionSink` where render publication is the target. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tests.rs`, driver tests. - -## Test Requirements - -### Unit Tests - -- Existing render manager tests pass. - -### Integration Tests - -- `cargo test -p prometeu-system -p prometeu-firmware -p prometeu-drivers`. - -### Manual Verification - -- `rg "publish_render_submission" crates/console` shows only legacy implementation or intentionally unmigrated scaffold. - -## Acceptance Criteria - -- [ ] Runtime publication uses `RenderSubmissionSink`. -- [ ] Firmware/Hub publication no longer depend directly on the monolithic bridge. -- [ ] Tests pass. - -## Dependencies - -- `PLN-0099`. - -## Risks - -- Firmware code may need a small context shape change. Keep compatibility adapters temporary and explicit. diff --git a/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md b/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md deleted file mode 100644 index bb4ffa76..00000000 --- a/discussion/workflow/plans/PLN-0101-remove-immediate-gfx-syscall-rendering.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -id: PLN-0101 -ticket: real-render-worker-establishment -title: Remove Immediate Gfx2D and GfxUI Syscall Rendering -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [runtime, gfx, syscalls, renderer] ---- - -## Objective - -Make `gfx2d.*` and `gfxui.*` syscalls buffer commands only, without mutating the framebuffer immediately. - -## Background - -`DEC-0032` requires framebuffer mutation to occur through render submission consumption/publication, not inside VM syscall dispatch. This is required before render can run independently. - -## Scope - -### Included - -- Remove `hw.gfx_mut().*` immediate draw calls from VM dispatch for `gfx2d` and `gfxui`. -- Add regression tests for `gfx2d.clear` and `gfx2d.draw_text`. -- Update tests that assumed immediate pixel mutation. - -### Excluded - -- Do not remove `GfxBridge`. -- Do not migrate composer in this plan. -- Do not implement worker. - -## Execution Steps - -### Step 1 - Remove immediate Game gfx writes - -**What:** Delete direct `hw.gfx_mut()` rendering in `Gfx*` syscall handlers. -**How:** Keep command buffering into `gfx2d_commands`. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. - -### Step 2 - Remove immediate Shell UI writes - -**What:** Delete direct `hw.gfx_mut()` rendering in `GfxUi*` syscall handlers. -**How:** Keep command buffering into `gfxui_commands`. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. - -### Step 3 - Add regression tests - -**What:** Prove syscalls do not affect pixels until publication. -**How:** Tests for `gfx2d.clear` and `gfx2d.draw_text` should inspect command buffers and frame publication. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tests.rs`. - -## Test Requirements - -### Unit Tests - -- `gfx2d.clear` buffers only. -- `gfx2d.draw_text` buffers only. -- Shell UI commands buffer only. - -### Integration Tests - -- Stress cartridge still renders after frame publication. - -### Manual Verification - -- `rg "gfx_mut\\(\\).*draw|gfx_mut\\(\\).*clear|gfx_mut\\(\\).*fill" dispatch.rs` returns no immediate render calls. - -## Acceptance Criteria - -- [ ] Gfx syscalls no longer mutate framebuffer immediately. -- [ ] Pixels change only after render submission publication. -- [ ] Tests updated to the new contract. - -## Dependencies - -- `PLN-0100`. - -## Risks - -- Debug workflows may have relied on immediate drawing. Tests must make the new frame boundary explicit. diff --git a/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md b/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md deleted file mode 100644 index afad26c2..00000000 --- a/discussion/workflow/plans/PLN-0102-introduce-game2d-frame-composer-service.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -id: PLN-0102 -ticket: real-render-worker-establishment -title: Introduce Game2DFrameComposer Logical Service -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [runtime, composer, game2d, platform] ---- - -## Objective - -Introduce `Game2DFrameComposer` as the logical-side service/facade for Game2D frame preparation. - -## Background - -`DEC-0032` states that `bind_scene`, `set_camera`, `emit_sprite`, and `close_game2d_packet` are logical frame preparation, not physical backend behavior. - -## Scope - -### Included - -- Define a `Game2DFrameComposer` facade. -- Move or wrap existing `FrameComposer` behavior under logical-side naming. -- Migrate composer syscalls to use the facade rather than generic hardware. - -### Excluded - -- Do not rewrite scene cache internals. -- Do not move viewport cache across threads. -- Do not implement worker. - -## Execution Steps - -### Step 1 - Define composer facade - -**What:** Add `Game2DFrameComposer` trait or service contract. -**How:** Include `begin_frame`, `bind_scene`, `unbind_scene`, `set_camera`, `emit_sprite`, `close_game2d_packet`, and required read checks. -**File(s):** `crates/console/prometeu-hal/src/platform.rs` or composer-specific HAL module. - -### Step 2 - Implement using current composer - -**What:** Make current local driver composer satisfy the new logical service. -**How:** Reuse `FrameComposer` implementation under adapter/implementation. -**File(s):** `crates/console/prometeu-drivers/src/frame_composer.rs`, `crates/console/prometeu-drivers/src/hardware.rs`. - -### Step 3 - Migrate composer syscalls - -**What:** Dispatch composer syscalls through `Game2DFrameComposer`. -**How:** Remove dependency on `HardwareBridge` methods for composer. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. - -## Test Requirements - -### Unit Tests - -- Composer facade closes packets equivalent to previous `FrameComposer`. -- Invalid scene/bank behavior remains unchanged. - -### Integration Tests - -- Existing scene/composer runtime tests pass. - -### Manual Verification - -- Composer syscalls no longer require `&mut dyn HardwareBridge`. - -## Acceptance Criteria - -- [ ] `Game2DFrameComposer` exists as logical service/facade. -- [ ] Composer syscalls route through it. -- [ ] Existing Game2D scene rendering remains correct. - -## Dependencies - -- `PLN-0098`. -- `PLN-0101`. - -## Risks - -- Scene cache ownership can be accidentally moved too early. Keep cache behavior local while changing the boundary. diff --git a/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md b/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md deleted file mode 100644 index 86224a60..00000000 --- a/discussion/workflow/plans/PLN-0103-introduce-runtime-platform-and-testplatform.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -id: PLN-0103 -ticket: real-render-worker-establishment -title: Introduce RuntimePlatform and TestPlatform -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [runtime, platform, tests] ---- - -## Objective - -Introduce `RuntimePlatform`/`Platform` as the aggregate of explicit platform services, and `TestPlatform` as the default test fixture. - -## Background - -`DEC-0032` requires tests and runtime integration to depend on explicit facades rather than `Hardware::new()` and `HardwareBridge`. - -## Scope - -### Included - -- Define aggregate platform access shape. -- Add `TestPlatform` in `prometeu-drivers`. -- Start migrating representative VM runtime tests. - -### Excluded - -- Do not migrate all tests in this plan. -- Do not remove `HardwareBridge`. -- Do not migrate all host code. - -## Execution Steps - -### Step 1 - Define platform aggregate - -**What:** Add `RuntimePlatform` or `Platform` aggregate facade. -**How:** Expose accessors for render sink, Game2D composer, audio, input/touch, assets/storage, clock/pacing, and telemetry placeholders. -**File(s):** `crates/console/prometeu-hal/src/platform.rs`. - -### Step 2 - Add TestPlatform - -**What:** Provide ergonomic test fixture implementing the initial platform facades. -**How:** Reuse current local driver components internally without exposing `HardwareBridge`. -**File(s):** `crates/console/prometeu-drivers/src/test_platform.rs`, `crates/console/prometeu-drivers/src/lib.rs`. - -### Step 3 - Migrate representative tests - -**What:** Move a small set of runtime tests from `Hardware::new()` to `TestPlatform`. -**How:** Choose tests covering render, composer, and asset surfaces. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tests.rs`, asset/memcard test modules as appropriate. - -## Test Requirements - -### Unit Tests - -- `TestPlatform` initializes all required facades. - -### Integration Tests - -- Migrated runtime tests pass with `TestPlatform`. - -### Manual Verification - -- New tests do not import `prometeu_drivers::Hardware` directly. - -## Acceptance Criteria - -- [ ] `RuntimePlatform`/`Platform` aggregate exists. -- [ ] `TestPlatform` exists and is used by representative tests. -- [ ] Test ergonomics remain acceptable. - -## Dependencies - -- `PLN-0098`. -- `PLN-0099`. -- `PLN-0102`. - -## Risks - -- Overly broad aggregate can recreate `HardwareBridge`. Keep domain access explicit and named. diff --git a/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md b/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md deleted file mode 100644 index 1cdc4475..00000000 --- a/discussion/workflow/plans/PLN-0104-migrate-vm-runtime-hostcontext-to-platform.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -id: PLN-0104 -ticket: real-render-worker-establishment -title: Migrate VM Runtime HostContext to Platform Services -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [runtime, vm, platform, hostcontext] ---- - -## Objective - -Change VM runtime execution from receiving `&mut dyn HardwareBridge` to receiving explicit platform services. - -## Background - -`HostContext` currently carries `Option<&mut dyn HardwareBridge>`. That shape lets every syscall see the entire bridge. `DEC-0032` requires explicit services instead. - -## Scope - -### Included - -- Introduce a platform-aware `HostContext` shape. -- Migrate VM runtime tick/dispatch to use platform services. -- Preserve VM tests and fault behavior. - -### Excluded - -- Do not migrate firmware/Hub in this plan. -- Do not remove `HardwareBridge` yet. - -## Execution Steps - -### Step 1 - Add platform host context - -**What:** Extend or replace `HostContext` with platform-service access. -**How:** Add methods for required VM syscall domains instead of `require_hw()`. -**File(s):** `crates/console/prometeu-hal/src/host_context.rs`. - -### Step 2 - Migrate runtime tick - -**What:** Tick uses platform services for begin-frame, composer frame closure, render sink, audio clear, and telemetry reads. -**How:** Replace direct bridge access with explicit facade calls. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tick.rs`. - -### Step 3 - Migrate dispatch - -**What:** VM syscall dispatch uses explicit facade methods. -**How:** Remove `ctx.require_hw()` as the central dispatch dependency. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. - -## Test Requirements - -### Unit Tests - -- VM dispatch tests pass with platform context. - -### Integration Tests - -- `cargo test -p prometeu-system`. - -### Manual Verification - -- `rg "require_hw|HardwareBridge" vm_runtime` shows no production VM runtime dependency except temporary compatibility adapters explicitly marked. - -## Acceptance Criteria - -- [ ] VM runtime dispatch no longer depends on the monolithic bridge. -- [ ] Tick uses explicit platform services. -- [ ] Existing VM fault behavior remains unchanged. - -## Dependencies - -- `PLN-0103`. - -## Risks - -- `HostContext` is used in VM unit tests. Migration must keep no-hardware test contexts ergonomic. diff --git a/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md b/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md deleted file mode 100644 index 1a574ea0..00000000 --- a/discussion/workflow/plans/PLN-0105-migrate-firmware-and-hub-to-platform-services.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -id: PLN-0105 -ticket: real-render-worker-establishment -title: Migrate Firmware and Hub to Platform Services -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [firmware, hub, platform, render] ---- - -## Objective - -Move firmware screens and Hub UI away from `HardwareBridge` and onto explicit platform services. - -## Background - -Firmware splash/crash and Hub currently receive `&mut dyn HardwareBridge` and publish Shell UI directly through it. `DEC-0032` requires migration to platform facades. - -## Scope - -### Included - -- Firmware context uses platform services. -- Splash/crash screens publish via render sink. -- Hub input/render uses input/render facades. - -### Excluded - -- Do not implement worker. -- Do not change Hub UX or firmware state machine semantics. - -## Execution Steps - -### Step 1 - Update firmware context - -**What:** Replace `PrometeuContext` hardware bridge field with platform services. -**How:** Expose only the services firmware needs: input, assets, render sink. -**File(s):** `crates/console/prometeu-firmware/src/firmware/prometeu_context.rs`, `firmware.rs`. - -### Step 2 - Migrate firmware screens - -**What:** Splash and crash screens publish through `RenderSubmissionSink`. -**How:** Build Shell UI packet and submit owned submission. -**File(s):** `crates/console/prometeu-firmware/src/firmware_step_splash_screen.rs`, `firmware_step_crash_screen.rs`. - -### Step 3 - Migrate Hub - -**What:** Hub uses platform input/render services instead of `HardwareBridge`. -**How:** Replace activation input and render publication dependencies. -**File(s):** `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`. - -## Test Requirements - -### Unit Tests - -- Firmware state tests pass. -- Hub UI tests pass. - -### Integration Tests - -- `cargo test -p prometeu-firmware -p prometeu-system`. - -### Manual Verification - -- Firmware/Hub no longer call `publish_render_submission` on `HardwareBridge`. - -## Acceptance Criteria - -- [ ] Firmware context no longer exposes the monolithic bridge. -- [ ] Splash/crash/Hub render via render sink. -- [ ] Hub input uses platform/input facade. - -## Dependencies - -- `PLN-0103`. -- `PLN-0104`. - -## Risks - -- Firmware code is lifecycle-sensitive. Keep behavior changes out of scope. diff --git a/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md b/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md deleted file mode 100644 index 499a22a9..00000000 --- a/discussion/workflow/plans/PLN-0106-migrate-desktop-host-to-platform-services.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -id: PLN-0106 -ticket: real-render-worker-establishment -title: Migrate Desktop Host to Platform Services -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [host, desktop, platform] ---- - -## Objective - -Move the desktop host from directly exposing `prometeu_drivers::Hardware` as the runtime bridge to owning a platform implementation. - -## Background - -The host currently creates `Hardware::new()` and passes it into firmware/runtime paths. `DEC-0032` requires hosts to implement platform services, not to expose a monolithic bridge. - -## Scope - -### Included - -- Introduce desktop/local platform ownership in the host. -- Replace host usage of `Hardware::W/H` with platform display constants or framebuffer descriptor. -- Keep the same visual/audio/input behavior. - -### Excluded - -- Do not implement render worker thread. -- Do not change windowing library or presentation policy. - -## Execution Steps - -### Step 1 - Add host platform field - -**What:** Host runner owns a platform implementation rather than raw `Hardware`. -**How:** Wrap current local driver implementation behind platform facades. -**File(s):** `crates/host/prometeu-host-desktop-winit/src/runner.rs`. - -### Step 2 - Replace direct hardware constants - -**What:** Stop reading `Hardware::W/H` directly in host rendering/input. -**How:** Use a framebuffer/display descriptor exposed by platform/render service. -**File(s):** `crates/host/prometeu-host-desktop-winit/src/runner.rs`, `input.rs`. - -### Step 3 - Update debugger host setup - -**What:** Debugger paths instantiate platform fixture/implementation. -**How:** Replace `Hardware::new()` in debugger setup. -**File(s):** `crates/host/prometeu-host-desktop-winit/src/debugger.rs`. - -## Test Requirements - -### Unit Tests - -- Host crate tests compile. - -### Integration Tests - -- `cargo test -p prometeu-host-desktop-winit`. - -### Manual Verification - -- Desktop host still runs stress cartridge. - -## Acceptance Criteria - -- [ ] Desktop host owns platform services. -- [ ] Host no longer treats `Hardware` as runtime bridge. -- [ ] Display dimensions come from platform/render descriptor. - -## Dependencies - -- `PLN-0105`. - -## Risks - -- Desktop host mixes runtime and window concerns. Keep worker threading out of this plan. diff --git a/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md b/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md deleted file mode 100644 index a19982cf..00000000 --- a/discussion/workflow/plans/PLN-0107-migrate-remaining-platform-domains.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -id: PLN-0107 -ticket: real-render-worker-establishment -title: Migrate Remaining Platform Domains -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [platform, audio, input, assets, clock, telemetry] ---- - -## Objective - -Migrate non-render domains out of `HardwareBridge` into explicit platform services. - -## Background - -`DEC-0032` permits render to migrate first but requires the full migration to cover audio, input/touch, assets/storage, clock/pacing, and telemetry where applicable. - -## Scope - -### Included - -- Audio service/facade migration. -- Input/touch service/facade migration. -- Assets/storage service/facade migration. -- Clock/pacing and telemetry surface migration where currently coupled. - -### Excluded - -- Do not change domain semantics. -- Do not implement async IO unless already supported by existing services. - -## Execution Steps - -### Step 1 - Migrate audio - -**What:** Replace `hw.audio()`/`hw.audio_mut()` production dependencies. -**How:** Route through explicit audio platform service. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`, firmware/host integration files. - -### Step 2 - Migrate input and touch - -**What:** Replace `hw.pad()`/`hw.touch()` dependencies. -**How:** Expose input snapshots through platform input service. -**File(s):** `crates/console/prometeu-system`, `crates/console/prometeu-firmware`, host input modules. - -### Step 3 - Migrate assets/storage - -**What:** Replace `hw.assets()`/`hw.assets_mut()` dependencies. -**How:** Route through asset/storage services while preserving commit and bank visibility semantics. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime`, `crates/console/prometeu-firmware`. - -### Step 4 - Migrate clock/pacing/telemetry coupling - -**What:** Move any platform-facing clock or telemetry coupling out of `HardwareBridge`. -**How:** Use explicit runtime/platform services. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime`, HAL telemetry modules. - -## Test Requirements - -### Unit Tests - -- Audio/input/asset syscall tests pass. - -### Integration Tests - -- `cargo test -p prometeu-system -p prometeu-firmware -p prometeu-drivers`. - -### Manual Verification - -- `rg "audio_mut|pad_mut|touch_mut|assets_mut" crates/console` does not show production bridge dependencies. - -## Acceptance Criteria - -- [ ] Audio migrated to platform service. -- [ ] Input/touch migrated to platform service. -- [ ] Assets/storage migrated to platform service. -- [ ] Clock/pacing/telemetry bridge coupling removed where applicable. - -## Dependencies - -- `PLN-0104`. -- `PLN-0105`. -- `PLN-0106`. - -## Risks - -- Asset visibility semantics are high risk. Keep behavior-preserving tests broad. diff --git a/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md b/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md deleted file mode 100644 index ce6871a0..00000000 --- a/discussion/workflow/plans/PLN-0108-migrate-tests-to-testplatform.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -id: PLN-0108 -ticket: real-render-worker-establishment -title: Migrate Tests to TestPlatform -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [tests, platform, fixtures] ---- - -## Objective - -Replace remaining test dependence on `Hardware::new()` and `HardwareBridge` with `TestPlatform` or narrower facade fixtures. - -## Background - -`DEC-0032` requires tests that need a full local platform fixture to use `TestPlatform`, not hidden `HardwareBridge` compatibility. - -## Scope - -### Included - -- VM runtime tests. -- Firmware tests. -- Driver integration tests that still use monolithic hardware only as fixture. -- Host debugger tests where applicable. - -### Excluded - -- Do not remove production bridge until tests are migrated. -- Do not change behavior under test. - -## Execution Steps - -### Step 1 - Inventory test dependencies - -**What:** Find all direct `Hardware::new()` and `HardwareBridge` test uses. -**How:** Use `rg` and classify by domain. -**File(s):** test modules across `crates/console` and `crates/host`. - -### Step 2 - Migrate full-platform tests - -**What:** Replace `Hardware::new()` with `TestPlatform`. -**How:** Use explicit facade accessors in assertions. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tests*.rs`, firmware tests. - -### Step 3 - Migrate narrow tests - -**What:** Replace full platform with narrower fixtures where only one domain is under test. -**How:** Use render/composer/audio/input/asset fixtures directly. -**File(s):** driver and runtime test modules. - -## Test Requirements - -### Unit Tests - -- All migrated tests pass. - -### Integration Tests - -- `cargo test --workspace` or relevant workspace subset if full workspace is too broad. - -### Manual Verification - -- `rg "Hardware::new\\(|HardwareBridge" crates --glob '*test*'` shows no test dependency except tests explicitly covering legacy removal. - -## Acceptance Criteria - -- [ ] `TestPlatform` is the default full platform fixture. -- [ ] Tests no longer preserve `HardwareBridge` by habit. -- [ ] Narrow tests use narrower fixtures. - -## Dependencies - -- `PLN-0103`. -- `PLN-0107`. - -## Risks - -- Large mechanical changes can hide behavior regressions. Keep commits grouped by test domain. diff --git a/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md b/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md deleted file mode 100644 index a44508da..00000000 --- a/discussion/workflow/plans/PLN-0109-remove-hardwarebridge-and-update-specs.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -id: PLN-0109 -ticket: real-render-worker-establishment -title: Remove HardwareBridge and Update Specs -status: done -created: 2026-06-06 -completed: -ref_decisions: [DEC-0032] -tags: [platform, cleanup, specs, hal] ---- - -## Objective - -Remove `HardwareBridge` from the codebase and publish the new platform layer contract in specs. - -## Background - -`DEC-0032` explicitly forbids preserving `HardwareBridge` as final compatibility. After all domains and tests migrate, the bridge must be deleted. - -## Scope - -### Included - -- Delete `HardwareBridge`. -- Remove exports/imports and adapters that only preserve bridge compatibility. -- Update canonical specs in English. -- Run validation and broad tests. - -### Excluded - -- Do not implement the real render worker. -- Do not change platform service semantics beyond cleanup. - -## Execution Steps - -### Step 1 - Delete bridge - -**What:** Remove `HardwareBridge` trait and module. -**How:** Delete source file and remove exports/imports. -**File(s):** `crates/console/prometeu-hal/src/hardware_bridge.rs`, `crates/console/prometeu-hal/src/lib.rs`, all remaining imports. - -### Step 2 - Remove compatibility adapters - -**What:** Delete migration-only adapters. -**How:** Replace final references with direct platform services or remove dead code. -**File(s):** HAL, drivers, system, firmware, host modules touched by previous plans. - -### Step 3 - Update specs - -**What:** Document platform layer and removal of monolithic hardware bridge. -**How:** Update runtime/host/render specs in English. -**File(s):** `docs/specs/runtime/*.md` and related canonical specs. - -### Step 4 - Validate and test - -**What:** Run discussion validation and broad test suite. -**How:** Use `discussion validate` and cargo tests for affected crates. -**File(s):** repository root. - -## Test Requirements - -### Unit Tests - -- HAL, drivers, firmware, system tests pass. - -### Integration Tests - -- Host desktop crate compiles/tests. -- Stress cartridge still runs through local platform path. - -### Manual Verification - -- `rg "HardwareBridge"` returns no production references. -- `rg "Hardware::new\\("` returns no runtime-facing dependency; any remaining use is local implementation detail or removed. - -## Acceptance Criteria - -- [ ] `HardwareBridge` is deleted. -- [ ] No production/runtime dependencies remain. -- [ ] Specs describe platform services/facades. -- [ ] Tests pass. -- [ ] `AGD-0043` can proceed without needing `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state in the worker. - -## Dependencies - -- `PLN-0107`. -- `PLN-0108`. - -## Risks - -- Removing the bridge too early will cause broad breakage. Only execute after dependency searches show all call sites have migrated. diff --git a/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md b/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md deleted file mode 100644 index 77e020d7..00000000 --- a/discussion/workflow/plans/PLN-0110-define-owned-rgba8888-frame-contract.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -id: PLN-0110 -ticket: real-render-worker-establishment -title: Define Owned RGBA8888 Frame Contract -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, hal, frame-contract] ---- - -## Briefing - -`DEC-0033` requires the worker to publish an owned RGBA8888 frame that is independent of native window APIs. This plan defines that shared HAL contract before any worker implementation depends on it. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Introduce `OwnedRgba8888Frame` in `prometeu-hal` as the canonical worker-to-host published frame type. - -## Escopo - -- Add the shared frame type to HAL. -- Include frame id, ownership/epoch metadata, dimensions, stride, and owned `Vec` RGBA8888 pixels. -- Add constructors and validation helpers that keep dimensions, stride, and pixel length coherent. -- Add unit tests for layout and validation. - -## Fora de Escopo - -- No worker thread. -- No host upload/present integration. -- No resource bank access. -- No spec updates beyond code-facing rustdoc if useful. - -## Plano de Execucao - -### Step 1 - Add HAL frame module - -**What:** Add `OwnedRgba8888Frame`. -**How:** Create a HAL module, likely `crates/console/prometeu-hal/src/owned_frame.rs` or equivalent, and export it from `lib.rs`. -**File(s):** `crates/console/prometeu-hal/src/lib.rs`, new HAL frame module. - -### Step 2 - Define required fields - -**What:** Encode the DEC-0033 frame contract. -**How:** Include `FrameId`, render ownership/epoch metadata compatible with `RenderSubmission`, `width`, `height`, `stride_pixels`, and `pixels: Vec`. -**File(s):** HAL frame module, existing render ownership types if reuse is needed. - -### Step 3 - Add constructors and validation - -**What:** Prevent malformed frame buffers from entering the worker-host boundary. -**How:** Add checked constructor(s) that reject zero dimensions, too-small stride, and pixel buffers shorter than `stride_pixels * height`. -**File(s):** HAL frame module. - -### Step 4 - Add tests - -**What:** Pin layout semantics. -**How:** Test valid construction, invalid stride, invalid pixel length, and that raw pixel values are preserved as RGBA8888 `u32`. -**File(s):** HAL frame module tests. - -## Criterios de Aceite - -- [ ] `OwnedRgba8888Frame` is exported by `prometeu-hal`. -- [ ] The type contains `FrameId`, ownership/epoch, width, height, `stride_pixels`, and `Vec` pixels. -- [ ] Invalid dimensions/stride/pixel length cannot be silently accepted by checked constructors. -- [ ] No native host/window/pixels/winit/SDL type appears in the HAL frame contract. - -## Tests / Validacao - -- `cargo test -p prometeu-hal` -- `rg "OwnedRgba8888Frame" crates/console/prometeu-hal -n` - -## Riscos - -- Reusing native pixel terminology could blur the logical RGBA8888 contract. -- Overfitting the frame type to current desktop upload code would leak host details into HAL. diff --git a/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md b/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md deleted file mode 100644 index 469ab083..00000000 --- a/discussion/workflow/plans/PLN-0111-define-read-only-render-resource-access.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -id: PLN-0111 -ticket: real-render-worker-establishment -title: Define Read-Only Render Resource Access -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, hal, resources] ---- - -## Briefing - -`DEC-0033` requires the worker backend to resolve render resources by stable ids through compact read-only access, without copying banks, snapshotting banks, requiring `Arc` in the public contract, or accessing mutable `Hardware`/`Gfx`/`FrameComposer`. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Define shared read-only render resource traits in HAL and implement them over existing local bank storage without changing bank ownership semantics. - -## Escopo - -- Define compact HAL traits for read-only glyph and scene bank access by id. -- Keep the public contract id-based and read-only. -- Implement the traits in the local driver/resource owner currently used by rendering. -- Add tests proving banks are read through the new interface. - -## Fora de Escopo - -- No worker thread. -- No bank copying or snapshotting. -- No public `Arc` requirement. -- No asset residency policy changes. - -## Plano de Execucao - -### Step 1 - Define HAL read-only traits - -**What:** Add worker-facing resource access contracts. -**How:** Define traits for resolving glyph and scene banks by stable ids. Return borrowed read-only references or compact read-only views, not mutable handles. -**File(s):** `crates/console/prometeu-hal/src/*`, likely a new render resource module. - -### Step 2 - Implement local resource access - -**What:** Expose current local bank storage through the new traits. -**How:** Implement the read-only traits for the existing memory bank/resource owner used by `prometeu-drivers`. -**File(s):** `crates/console/prometeu-drivers/src/memory_banks.rs`, `frame_composer.rs`, or adjacent resource modules. - -### Step 3 - Thread access through backend-facing APIs - -**What:** Make future backends able to receive resource access without `Hardware`. -**How:** Add narrow parameters/types where needed so a backend can be given read-only resources independently from the full platform. -**File(s):** HAL contracts and driver modules touched above. - -### Step 4 - Add resource access tests - -**What:** Prove id lookup and read-only behavior. -**How:** Add tests for resident/missing glyph and scene banks through the new traits and verify no mutable API is exposed. -**File(s):** HAL/driver tests. - -## Criterios de Aceite - -- [ ] Worker-facing render resource access is defined in `prometeu-hal`. -- [ ] Resource access is by stable id and read-only. -- [ ] The public contract does not require `Arc`, bank copies, bank snapshots, `Hardware`, `Gfx`, or `FrameComposer`. -- [ ] Local driver storage implements the traits. - -## Tests / Validacao - -- `cargo test -p prometeu-hal -p prometeu-drivers` -- `rg "Arc<.*RenderResource|snapshot.*bank|&mut Hardware|&mut Gfx|FrameComposer" crates/console/prometeu-hal crates/console/prometeu-drivers -n` - -## Riscos - -- Returning concrete bank internals may overexpose driver implementation. -- Accidentally requiring `Arc` in the trait would make a storage strategy part of the public contract. diff --git a/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md b/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md deleted file mode 100644 index 6d8df73d..00000000 --- a/discussion/workflow/plans/PLN-0112-define-render-worker-errors-and-telemetry.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: PLN-0112 -ticket: real-render-worker-establishment -title: Define Render Worker Errors and Telemetry -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, hal, telemetry] ---- - -## Briefing - -`DEC-0033` requires typed worker/backend errors and telemetry that observes worker behavior without changing VM semantics. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Introduce the shared error and telemetry vocabulary required by the real worker. - -## Escopo - -- Define `RenderWorkerError` or equivalent shared error type. -- Define worker telemetry counters/snapshot shape. -- Integrate the new telemetry shape with existing render telemetry types where appropriate. -- Add tests for error/debug/copy/equality behavior and telemetry defaults. - -## Fora de Escopo - -- No worker lifecycle implementation. -- No thread-safe handoff. -- No host integration. - -## Plano de Execucao - -### Step 1 - Add worker error type - -**What:** Define typed worker errors. -**How:** Add variants for backend unavailable, render failed, publish/present handoff failed, shutdown timeout, stale submission discarded, and worker panic/internal failure. -**File(s):** `crates/console/prometeu-hal/src/*` or `prometeu-system` if the type is not shared; prefer HAL if host/tests need it. - -### Step 2 - Add telemetry snapshot - -**What:** Define counters required by DEC-0033. -**How:** Include produced, replaced/dropped, consumed, published/presented, stale discarded, render failures, repeated frames, and shutdown timeouts. -**File(s):** existing telemetry/render manager modules or new worker telemetry module. - -### Step 3 - Preserve existing telemetry compatibility - -**What:** Avoid breaking existing render telemetry tests. -**How:** Map or extend current render telemetry without changing VM semantics or current counters unexpectedly. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs`, HAL telemetry modules as needed. - -### Step 4 - Add tests - -**What:** Pin type behavior. -**How:** Test default zero telemetry, counter increments through helper methods if added, and error formatting/debug behavior. -**File(s):** HAL/system tests. - -## Criterios de Aceite - -- [ ] Worker/backend failure has a typed error surface. -- [ ] Telemetry includes all DEC-0033 minimum counters. -- [ ] Existing render telemetry tests still pass. -- [ ] No worker/backend failure path relies on panic as the normal reporting model. - -## Tests / Validacao - -- `cargo test -p prometeu-hal -p prometeu-system` -- `rg "RenderWorkerError|shutdown_timeout|stale.*discard|repeat" crates/console -n` - -## Riscos - -- Splitting telemetry between too many structs may make final worker accounting inconsistent. -- Adding errors before call sites exist may create unused-code warnings if not scoped carefully. diff --git a/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md b/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md deleted file mode 100644 index cb60d927..00000000 --- a/discussion/workflow/plans/PLN-0113-implement-thread-safe-latest-wins-handoff.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -id: PLN-0113 -ticket: real-render-worker-establishment -title: Implement Thread-Safe Latest-Wins Handoff -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, concurrency, handoff] ---- - -## Briefing - -`DEC-0033` requires a thread-safe single-slot latest-wins handoff. The producer must replace pending work without waiting for rasterization or present, and the consumer must take ownership of the latest pending submission. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Add the real worker handoff primitive in `prometeu-system`, using explicit synchronization and deterministic tests. - -## Escopo - -- Implement a single-slot pending submission structure. -- Use `Mutex> + Condvar` or an equivalent explicit structure. -- Add producer publish/replace behavior and consumer wait/take behavior. -- Track replacement/drop telemetry. - -## Fora de Escopo - -- No worker thread controller lifecycle. -- No backend rendering. -- No host integration. -- No stale epoch publish behavior beyond storing ownership data already present in submissions. - -## Plano de Execucao - -### Step 1 - Add handoff module - -**What:** Create the worker handoff primitive. -**How:** Add a module under `crates/console/prometeu-system/src/services/vm_runtime/`, for example `render_worker_handoff.rs`. -**File(s):** new system module, `mod.rs` exports. - -### Step 2 - Implement publish replacement - -**What:** Producer publishes owned `RenderSubmission`. -**How:** Lock the slot, replace any pending submission, increment replacement/drop counters when applicable, notify the condvar, and return immediately. -**File(s):** handoff module. - -### Step 3 - Implement consumer wait/take - -**What:** Consumer waits for work or shutdown signal. -**How:** Add wait/take APIs that block only the consumer side and return owned submissions. -**File(s):** handoff module. - -### Step 4 - Add deterministic tests - -**What:** Prove latest-wins and blocking boundaries. -**How:** Use threads, barriers, and condvars to prove producer replacement and consumer wake behavior without sleeps. -**File(s):** handoff module tests or `async_render_contract_tests.rs`. - -## Criterios de Aceite - -- [ ] Producer publish does not wait for consumer rasterization. -- [ ] Multiple pending publishes leave only the newest submission available. -- [ ] Replacements increment telemetry. -- [ ] Consumer can wait for work and is woken by publish. -- [ ] Tests do not use sleep as their primary synchronization proof. - -## Tests / Validacao - -- `cargo test -p prometeu-system render_worker_handoff` -- `cargo test -p prometeu-system` - -## Riscos - -- Holding the mutex while doing heavy work would violate non-blocking producer behavior. -- Condvar wait loops must handle spurious wakeups. diff --git a/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md b/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md deleted file mode 100644 index cba2fb1e..00000000 --- a/discussion/workflow/plans/PLN-0114-add-deterministic-render-worker-test-harness.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: PLN-0114 -ticket: real-render-worker-establishment -title: Add Deterministic Render Worker Test Harness -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, tests, concurrency] ---- - -## Briefing - -`DEC-0033` requires worker behavior to be proven without a native window and without fragile sleeps. This plan creates reusable deterministic test fixtures for the remaining worker plans. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Add a fake backend and synchronization harness capable of blocking render at controlled points, observing publication, and proving non-blocking VM/producer behavior. - -## Escopo - -- Create test-only synchronization helpers using barriers/condvars. -- Create a fake render backend shape that can block/fail/publish deterministically. -- Add helper functions for constructing owned render submissions and frames. -- Reuse the harness in later worker tests. - -## Fora de Escopo - -- No production worker lifecycle. -- No desktop host integration. -- No real GFX rasterization. - -## Plano de Execucao - -### Step 1 - Add test support module - -**What:** Introduce deterministic worker test fixtures. -**How:** Add test-only helpers under `prometeu-system` worker tests or a local test module. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/*`. - -### Step 2 - Add controllable fake backend - -**What:** Provide backend behavior for tests. -**How:** Fake backend can block at render start, unblock by test signal, return errors, and record published frames. -**File(s):** worker test module. - -### Step 3 - Add submission/frame builders - -**What:** Make tests concise and consistent. -**How:** Build Game/Shell submissions with explicit frame ids and ownership metadata; build `OwnedRgba8888Frame` fixtures. -**File(s):** worker test module. - -### Step 4 - Add initial harness self-tests - -**What:** Prove harness itself is deterministic. -**How:** Verify a test can block and release backend render without timing sleeps. -**File(s):** worker test module. - -## Criterios de Aceite - -- [ ] Tests can block worker backend render using deterministic synchronization. -- [ ] Tests can observe published `OwnedRgba8888Frame` values. -- [ ] Harness can inject backend errors. -- [ ] Harness helpers are scoped to tests or clearly internal. - -## Tests / Validacao - -- `cargo test -p prometeu-system render_worker` -- `rg "sleep\\(|thread::sleep" crates/console/prometeu-system/src/services/vm_runtime -n` - -## Riscos - -- Test harness code can become too coupled to implementation internals. -- Overly broad helpers can hide important assertions in later plans. diff --git a/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md b/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md deleted file mode 100644 index 97bb37b3..00000000 --- a/discussion/workflow/plans/PLN-0115-implement-render-worker-controller-lifecycle.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -id: PLN-0115 -ticket: real-render-worker-establishment -title: Implement Render Worker Controller Lifecycle -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, lifecycle, concurrency] ---- - -## Briefing - -`DEC-0033` requires a runtime-owned worker controller in `prometeu-system`, with stop signaling, bounded shutdown, pending/in-flight separation, typed errors, and telemetry. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Implement the production worker controller lifecycle around the handoff primitive and backend trait shape. - -## Escopo - -- Add worker controller type in `prometeu-system`. -- Spawn and join a worker thread. -- Add configurable shutdown timeout with initial default `250ms`. -- Add stop token/wakeup behavior. -- Preserve pending/in-flight separation. -- Capture panic as typed internal failure. - -## Fora de Escopo - -- No desktop host presentation. -- No full runtime tick integration. -- No stale epoch/repeat behavior beyond lifecycle foundations. - -## Plano de Execucao - -### Step 1 - Define controller and config - -**What:** Add worker controller and configuration. -**How:** Include shutdown timeout, default `250ms`, and hooks for backend and handoff dependencies. -**File(s):** new `render_worker.rs` or similar in `prometeu-system`. - -### Step 2 - Implement start and run loop - -**What:** Start worker execution. -**How:** Spawn a thread that waits on the handoff, takes submissions, marks in-flight locally, calls backend, and publishes results through a frame sink abstraction. -**File(s):** worker controller module. - -### Step 3 - Implement stop and bounded join - -**What:** Make shutdown observable and bounded. -**How:** Signal stop, wake the condvar, join with timeout strategy available in Rust implementation, and return/report `ShutdownTimeout` when exceeded. -**File(s):** worker controller module. - -### Step 4 - Add panic containment - -**What:** Convert worker panic into typed failure. -**How:** Use panic containment around backend calls or worker thread join result and update telemetry/errors. -**File(s):** worker controller module. - -### Step 5 - Add lifecycle tests - -**What:** Prove start/stop/join behavior. -**How:** Use deterministic harness from `PLN-0114`. -**File(s):** worker tests. - -## Criterios de Aceite - -- [ ] Worker controller lives in `prometeu-system`. -- [ ] Controller can start and stop a real worker thread. -- [ ] Shutdown wakes waiting worker and completes within configured bound in tests. -- [ ] Shutdown timeout is typed and observable. -- [ ] Worker panic is captured as typed internal failure. - -## Tests / Validacao - -- `cargo test -p prometeu-system render_worker` -- `cargo test -p prometeu-system` - -## Riscos - -- Rust thread join timeout requires careful design because standard `JoinHandle::join` has no timeout. -- Backend ownership must be `'static + Send` without leaking host-specific types into system contracts. diff --git a/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md b/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md deleted file mode 100644 index 4aacc4bf..00000000 --- a/discussion/workflow/plans/PLN-0116-implement-fake-local-render-backend.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: PLN-0116 -ticket: real-render-worker-establishment -title: Implement Fake Local Render Backend -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, backend, drivers] ---- - -## Briefing - -`DEC-0033` requires fake/local backend behavior before desktop integration so worker semantics can be proven without a window. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Implement backend contracts that consume `RenderSubmission` plus read-only resources and produce `OwnedRgba8888Frame`. - -## Escopo - -- Define or finalize a backend trait for worker rendering. -- Add a fake backend for deterministic tests. -- Add a local framebuffer backend that can rasterize through existing driver/GFX logic without exposing mutable `Hardware` as the worker contract. -- Verify output frame ownership and pixels. - -## Fora de Escopo - -- No desktop upload/present. -- No runtime tick integration. -- No resource mutation policy changes. - -## Plano de Execucao - -### Step 1 - Define backend trait - -**What:** Encode backend consumption contract. -**How:** Trait consumes `RenderSubmission` and read-only resource access and returns `OwnedRgba8888Frame` or `RenderWorkerError`. -**File(s):** HAL if shared, or system if internal; prefer HAL if host/drivers implement it. - -### Step 2 - Implement fake backend - -**What:** Provide test backend. -**How:** Fake backend returns deterministic frames, can block through harness, and can return typed errors. -**File(s):** system test module or driver test utility. - -### Step 3 - Implement local framebuffer backend - -**What:** Provide non-window raster backend. -**How:** Use existing GFX rendering internally to turn submissions into `OwnedRgba8888Frame`, but expose only the backend trait and read-only resources. -**File(s):** `prometeu-drivers` rendering modules, possibly a new backend module. - -### Step 4 - Add output tests - -**What:** Prove frame content and metadata. -**How:** Render simple Game2D and ShellUi submissions and compare `OwnedRgba8888Frame` pixels/metadata. -**File(s):** driver/system tests. - -## Criterios de Aceite - -- [ ] Backend trait does not expose mutable `Hardware`, `Gfx`, `FrameComposer`, or VM state. -- [ ] Fake backend supports deterministic blocking/error behavior. -- [ ] Local backend produces `OwnedRgba8888Frame`. -- [ ] Game2D and ShellUi submissions can produce frame output without a native window. - -## Tests / Validacao - -- `cargo test -p prometeu-drivers -p prometeu-system` -- `rg "&mut Hardware|&mut Gfx|FrameComposer" crates/console/prometeu-hal crates/console/prometeu-system/src/services/vm_runtime -n` - -## Riscos - -- Local backend may be tempted to reuse `Hardware` directly as the public worker contract. -- Resource access gaps may surface when moving from fake to local rasterization. diff --git a/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md b/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md deleted file mode 100644 index 15509c7a..00000000 --- a/discussion/workflow/plans/PLN-0117-integrate-runtime-submission-with-worker.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -id: PLN-0117 -ticket: real-render-worker-establishment -title: Integrate Runtime Submission With Worker -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, integration] ---- - -## Briefing - -`DEC-0033` requires VM/render production to submit work without blocking on worker consumption. This plan connects runtime frame closure to the worker handoff while preserving local synchronous fallback. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Route eligible Game render submissions from `RenderManager` into the real worker handoff and keep Shell/local paths correct. - -## Escopo - -- Add worker-capable runtime path. -- Submit owned `RenderSubmission` to the handoff. -- Preserve AppMode policy: Game may use worker; Shell remains local unless a future decision changes it. -- Keep local synchronous fallback. -- Prove VM tick returns without waiting for worker backend completion. - -## Fora de Escopo - -- No desktop host presentation. -- No final stale/repeat behavior beyond integration needed for basic worker use. -- No spec edits. - -## Plano de Execucao - -### Step 1 - Add worker ownership to runtime state - -**What:** Store worker controller/handoff in runtime-owned state. -**How:** Extend `VirtualMachineRuntime` or `RenderManager` integration points without moving policy to host. -**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tick.rs`, `render_manager.rs`, worker modules. - -### Step 2 - Submit Game frames to worker handoff - -**What:** Replace local worker prototype path with real worker submission. -**How:** On eligible Game frame closure, publish owned `RenderSubmission` into the handoff and return to VM tick without waiting for consumption. -**File(s):** runtime tick/render manager integration. - -### Step 3 - Preserve local fallback - -**What:** Keep current local synchronous behavior available. -**How:** Use existing `RenderConsumerPath`/policy or update it to select local fallback when worker is disabled/unavailable. -**File(s):** `render_manager.rs`, `tick.rs`. - -### Step 4 - Add non-blocking tests - -**What:** Prove VM producer does not block. -**How:** Use harness to hold backend render, run tick/publish additional frames, assert return and replacement telemetry. -**File(s):** runtime worker tests. - -## Criterios de Aceite - -- [ ] Game render submissions can be sent to the real worker handoff. -- [ ] VM tick does not wait for worker rasterization. -- [ ] Shell/local policy remains unchanged. -- [ ] Local synchronous fallback still passes existing tests. -- [ ] Replacement telemetry increments when producer outruns worker. - -## Tests / Validacao - -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-firmware` - -## Riscos - -- Accidentally making frame closure wait for worker consumption would violate DEC-0033. -- Mixing Shell lifecycle rendering into the worker path too early would broaden scope. diff --git a/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md b/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md deleted file mode 100644 index 59301fa6..00000000 --- a/discussion/workflow/plans/PLN-0118-add-stale-epoch-and-repeat-frame-behavior.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -id: PLN-0118 -ticket: real-render-worker-establishment -title: Add Stale Epoch and Repeat Frame Behavior -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, ownership, telemetry] ---- - -## Briefing - -`DEC-0033` requires the worker to discard stale in-flight frames before publication and repeat the last published `OwnedRgba8888Frame` rather than recomposing. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Complete ownership/epoch checks and repeat-last-published-frame behavior for the worker path. - -## Escopo - -- Add shared active ownership/epoch observation for worker publication checks. -- Discard stale in-flight frames before publication. -- Store and expose the latest published `OwnedRgba8888Frame`. -- Implement repeat-last-frame behavior and telemetry. - -## Fora de Escopo - -- No desktop upload/present. -- No new AppMode policy. -- No native window invalidation changes. - -## Plano de Execucao - -### Step 1 - Expose ownership/epoch to worker - -**What:** Let worker validate in-flight submissions before publishing. -**How:** Add a thread-safe snapshot or handle that the worker can read without mutating runtime state. -**File(s):** `render_manager.rs`, worker modules. - -### Step 2 - Implement stale discard - -**What:** Prevent stale pixels from publication. -**How:** Before publishing frame output, compare submission ownership/epoch with active ownership/epoch; discard on mismatch and increment telemetry. -**File(s):** worker controller module. - -### Step 3 - Store latest published frame - -**What:** Make the latest valid frame available to host/fallback tests. -**How:** Add a published-frame slot distinct from pending submission and in-flight work. -**File(s):** worker controller or published frame module. - -### Step 4 - Implement repeat behavior - -**What:** Repeat last valid frame without recomposition. -**How:** Add API to fetch/reuse the latest published `OwnedRgba8888Frame`; increment repeat telemetry when cadence requests reuse. -**File(s):** worker modules, tests. - -### Step 5 - Add tests - -**What:** Prove stale discard and repeat. -**How:** Use harness to hold in-flight work, change owner/epoch, release backend, and assert no publication; separately assert repeat returns last frame. -**File(s):** worker tests. - -## Criterios de Aceite - -- [ ] Worker checks active owner/epoch before publishing. -- [ ] Stale in-flight frames are discarded and counted. -- [ ] Latest published frame is an `OwnedRgba8888Frame`. -- [ ] Repeat uses the last published frame and does not recompose. -- [ ] Tests cover stale discard and repeat deterministically. - -## Tests / Validacao - -- `cargo test -p prometeu-system render_worker` -- `cargo test -p prometeu-system` - -## Riscos - -- Reading active ownership from the wrong layer may reintroduce mutable runtime coupling. -- Repeat behavior must not hide render failures unless telemetry records them. diff --git a/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md b/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md deleted file mode 100644 index 843e2f09..00000000 --- a/discussion/workflow/plans/PLN-0119-integrate-desktop-host-worker-presentation.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -id: PLN-0119 -ticket: real-render-worker-establishment -title: Integrate Desktop Host Worker Presentation -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, host, desktop] ---- - -## Briefing - -`DEC-0033` is not complete until the real host path uses the worker. This plan integrates desktop presentation with the latest published `OwnedRgba8888Frame` while keeping native upload/present in the host event loop. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Make `prometeu-host-desktop-winit` present frames produced by the worker path. - -## Escopo - -- Wire the host to enable/use the worker path. -- Read the latest published `OwnedRgba8888Frame`. -- Upload/copy RGBA8888 pixels to the existing pixels/winit presentation surface in the event loop. -- Preserve host invalidation and redraw behavior. -- Keep worker free of native window dependencies. - -## Fora de Escopo - -- No SDL migration. -- No change to logical pixel format. -- No change to input/audio/filesystem host behavior. - -## Plano de Execucao - -### Step 1 - Add host access to published frame - -**What:** Expose latest worker-published frame to desktop host. -**How:** Add an API through runtime/platform integration that returns or copies the latest `OwnedRgba8888Frame` for host upload. -**File(s):** system worker modules, host runner integration. - -### Step 2 - Upload RGBA8888 in event loop - -**What:** Present worker output through existing host surface. -**How:** Convert/copy `Vec` RGBA8888 into the existing pixels frame buffer using existing RGBA8888 utility conventions. -**File(s):** `crates/host/prometeu-host-desktop-winit/src/runner.rs`, `utilities.rs` if needed. - -### Step 3 - Preserve invalidation model - -**What:** Keep host redraw policy coherent. -**How:** Trigger redraw when a new frame is published or host invalidation occurs; repeat uses the last published frame. -**File(s):** host runner/presentation state. - -### Step 4 - Add host tests - -**What:** Prove host consumes worker-published frame. -**How:** Add unit tests for RGBA8888 copy/upload helper and presentation state transitions without opening a native window. -**File(s):** host tests. - -## Criterios de Aceite - -- [ ] Desktop host presents the latest worker-published `OwnedRgba8888Frame`. -- [ ] Worker code does not import winit, pixels, SDL, swapchain, native texture, or window APIs. -- [ ] Host event loop remains responsible for native present. -- [ ] Existing host invalidation tests still pass. -- [ ] Real host path can run with worker enabled. - -## Tests / Validacao - -- `cargo test -p prometeu-host-desktop-winit` -- `cargo test -p prometeu-system -p prometeu-firmware` -- Manual run of a simple cartridge through desktop host with worker path enabled. - -## Riscos - -- Upload/copy can accidentally reinterpret RGBA8888 as native endian bytes. -- Host redraw policy can regress into perpetual polling if publication/invalidation are not kept separate. diff --git a/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md b/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md deleted file mode 100644 index 2392d491..00000000 --- a/discussion/workflow/plans/PLN-0120-update-real-render-worker-specs.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -id: PLN-0120 -ticket: real-render-worker-establishment -title: Update Real Render Worker Specs -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, specs] ---- - -## Briefing - -`DEC-0033` requires canonical runtime specs to document the real worker contract after implementation publishes the behavior. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Update English canonical specs to describe `OwnedRgba8888Frame`, worker handoff, host presentation split, read-only bank access, shutdown, errors, and telemetry. - -## Escopo - -- Update GFX/render specs. -- Update portability/host presentation specs. -- Update host ABI/syscall specs if worker behavior affects published runtime surfaces. -- Ensure no spec says the worker owns native window present. - -## Fora de Escopo - -- No new architecture beyond `DEC-0033`. -- No lesson writing. -- No implementation changes except documentation. - -## Plano de Execucao - -### Step 1 - Update GFX spec - -**What:** Document worker frame publication. -**How:** Add `OwnedRgba8888Frame`, RGBA8888 `Vec` semantics, latest published frame, and repeat behavior. -**File(s):** `docs/specs/runtime/04-gfx-peripheral.md`. - -### Step 2 - Update portability spec - -**What:** Document worker/host split. -**How:** State that worker produces owned RGBA8888 frames and host event loop performs native upload/present. -**File(s):** `docs/specs/runtime/11-portability-and-cross-platform-execution.md`. - -### Step 3 - Update concurrency/events spec if needed - -**What:** Document handoff and shutdown behavior. -**How:** Add single-slot latest-wins, bounded shutdown, and non-blocking producer semantics where concurrency is specified. -**File(s):** `docs/specs/runtime/09-events-and-concurrency.md` or adjacent runtime spec. - -### Step 4 - Update README/index references - -**What:** Keep spec map discoverable. -**How:** Add any needed cross-reference to worker/render sections. -**File(s):** `docs/specs/runtime/README.md`. - -## Criterios de Aceite - -- [ ] Specs define `OwnedRgba8888Frame`. -- [ ] Specs state worker does not own native window present. -- [ ] Specs state producer does not block on worker rasterization/present. -- [ ] Specs document read-only bank access by id. -- [ ] Specs document bounded shutdown and typed error expectations. - -## Tests / Validacao - -- `rg "OwnedRgba8888Frame|latest-wins|shutdown|read-only" docs/specs/runtime -n` -- `discussion validate` - -## Riscos - -- Updating specs before implementation may accidentally publish intent as completed behavior; execute after code plans that establish the behavior. -- Spec wording must remain English and normative. diff --git a/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md b/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md deleted file mode 100644 index 35253868..00000000 --- a/discussion/workflow/plans/PLN-0121-final-worker-path-validation-and-hardening.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -id: PLN-0121 -ticket: real-render-worker-establishment -title: Final Worker Path Validation and Hardening -status: done -created: 2026-06-15 -completed: -ref_decisions: [DEC-0033] -tags: [runtime, renderer, worker, validation, hardening] ---- - -## Briefing - -`DEC-0033` is complete only when the worker functions on the real host path and the contract is proven end to end. This plan performs final validation, residue scans, and hardening before housekeeping. - -## Decisions de Origem - -- `DEC-0033`: Real Render Worker Contract. - -## Alvo - -Prove the real render worker path satisfies DEC-0033 across HAL, system, drivers, firmware, host, specs, and tests. - -## Escopo - -- Run broad test suites. -- Add missing edge tests found during validation. -- Scan for forbidden coupling. -- Verify host real path uses worker. -- Verify local fallback still works. -- Prepare evidence for discussion housekeeping. - -## Fora de Escopo - -- No new worker architecture. -- No SDL migration. -- No unrelated performance tuning. - -## Plano de Execucao - -### Step 1 - Run full affected test suite - -**What:** Validate all touched crates. -**How:** Run workspace tests and focused worker/host tests. -**File(s):** repository root. - -### Step 2 - Run coupling residue scans - -**What:** Enforce DEC-0033 boundaries. -**How:** Search worker/system/HAL code for native window dependencies, mutable hardware/GFX/composer leaks, bank copying/snapshot terminology, and sleeps in concurrency tests. -**File(s):** repository root. - -### Step 3 - Add missing hardening tests - -**What:** Close any discovered gaps. -**How:** Add focused tests for missing telemetry/error/shutdown/host-path cases. -**File(s):** affected test modules. - -### Step 4 - Verify host path - -**What:** Prove desktop host uses worker output. -**How:** Run host tests and a manual/simple cartridge path with worker enabled; record evidence in final summary. -**File(s):** host runner and runtime integration. - -### Step 5 - Prepare housekeeping evidence - -**What:** Make `DSC-0042` ready for final lesson/housekeeping. -**How:** Record completed plan state and validation evidence for later `discussion-housekeep`. -**File(s):** discussion artifacts if needed. - -## Criterios de Aceite - -- [ ] `cargo test --workspace` passes. -- [ ] Focused worker, system, drivers, firmware, and host tests pass. -- [ ] Residue scans show worker code does not depend on native window APIs. -- [ ] Residue scans show worker boundary does not expose `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state. -- [ ] Desktop host path uses the real worker output. -- [ ] Local fallback still works. -- [ ] `discussion validate` passes. - -## Tests / Validacao - -- `cargo test --workspace` -- `cargo test -p prometeu-system -p prometeu-drivers -p prometeu-hal -p prometeu-firmware -p prometeu-host-desktop-winit` -- `discussion validate` -- `rg "winit|pixels|SDL|swapchain|native texture" crates/console/prometeu-system crates/console/prometeu-hal -n` -- `rg "&mut Hardware|&mut Gfx|FrameComposer" crates/console/prometeu-system/src/services/vm_runtime crates/console/prometeu-hal -n` -- `rg "thread::sleep|sleep\\(" crates/console/prometeu-system/src/services/vm_runtime -n` - -## Riscos - -- Final validation may expose earlier plan gaps; fix them in the narrowest affected plan/module. -- Manual host evidence can be flaky if tied to native window availability; keep automated host-unit evidence as the primary gate. - -## Validation Evidence - -- `cargo test --workspace`: passed. -- `cargo test -p prometeu-system -p prometeu-drivers -p prometeu-hal -p prometeu-firmware -p prometeu-host-desktop-winit`: passed. -- Native API coupling scan for worker/runtime console code: no `winit`, `pixels`, SDL, swapchain, or native texture API references found. -- Mutable boundary scan for worker/runtime console code: no `&mut Hardware`, `&mut Gfx`, or concrete live `FrameComposer` references found. -- Timing scan for render worker runtime tests: no `thread::sleep` / `sleep(` usage found. -- Added host-unit evidence that worker frame presentation is restricted to `GameRunning`; Shell/Hub paths keep local presentation behavior.