6.3 KiB
| id | ticket | title | created | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| LSN-0049 | real-render-worker-establishment | Render Workers Publish Frames, Hosts Present Them | 2026-06-20 |
|
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.