prometeu-runtime/discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md

105 lines
6.3 KiB
Markdown

---
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.