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