Real Render Worker Establishment
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
This commit is contained in:
parent
aa917cf44e
commit
506eb36786
@ -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;
|
||||
|
||||
@ -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<OwnedRgba8888Frame> {
|
||||
self.os.vm_runtime.latest_render_worker_frame()
|
||||
}
|
||||
|
||||
pub fn repeat_latest_render_worker_frame(&self) -> Option<OwnedRgba8888Frame> {
|
||||
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 {
|
||||
|
||||
@ -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<OwnedRgba8888Frame> {
|
||||
self.render_worker_controller.as_ref()?.latest_published_frame()
|
||||
}
|
||||
|
||||
pub fn repeat_latest_render_worker_frame(&self) -> Option<OwnedRgba8888Frame> {
|
||||
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(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -25,7 +25,28 @@ impl Default for RenderWorkerConfig {
|
||||
struct RenderWorkerState {
|
||||
telemetry: RenderWorkerTelemetry,
|
||||
last_error: Option<RenderWorkerError>,
|
||||
latest_published_frame: Option<OwnedRgba8888Frame>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LatestRenderFrameStore {
|
||||
latest: Arc<Mutex<Option<OwnedRgba8888Frame>>>,
|
||||
}
|
||||
|
||||
impl LatestRenderFrameStore {
|
||||
pub fn publish_frame(&self, frame: OwnedRgba8888Frame) {
|
||||
*self.latest.lock().unwrap() = Some(frame);
|
||||
}
|
||||
|
||||
pub fn latest_frame(&self) -> Option<OwnedRgba8888Frame> {
|
||||
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<OwnedRgba8888Frame> {
|
||||
self.state.lock().unwrap().latest_published_frame.clone()
|
||||
}
|
||||
|
||||
pub fn repeat_latest_frame(&self) -> Option<OwnedRgba8888Frame> {
|
||||
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<B, S>(
|
||||
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<Mutex<RenderWorkerState>>, frame: OwnedRgba8888Frame) {
|
||||
fn record_published(state: &Arc<Mutex<RenderWorkerState>>, 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<Mutex<RenderWorkerState>>, 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,26 +430,29 @@ 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]
|
||||
#[ignore] //TODO: enable it again
|
||||
fn runtime_publishes_game_submissions_to_worker_without_waiting_for_render() {
|
||||
let gate = RenderGate::default();
|
||||
let backend = Arc::new(FakeRenderBackend::with_gate(gate.clone()));
|
||||
@ -423,7 +469,6 @@ mod tests {
|
||||
Game2DFramePacket::default(),
|
||||
))
|
||||
.expect("game packet should close");
|
||||
assert!(runtime.publish_pending_render_to_worker());
|
||||
gate.wait_for_entries(1);
|
||||
|
||||
runtime
|
||||
@ -432,14 +477,12 @@ mod tests {
|
||||
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);
|
||||
|
||||
|
||||
@ -276,11 +276,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)
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
@ -306,9 +301,12 @@ impl ApplicationHandler for HostRunner {
|
||||
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();
|
||||
@ -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 {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -6,7 +6,7 @@ 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]
|
||||
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, PLN-0122]
|
||||
tags: [runtime, renderer, worker, concurrency, host, hal, architecture]
|
||||
---
|
||||
|
||||
@ -289,8 +289,10 @@ Recommended plan sequence:
|
||||
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.
|
||||
8. unify the worker publication store so the sink that receives published frames is the observable source for host presentation;
|
||||
9. update specs and final validation tests.
|
||||
|
||||
## Revision Log
|
||||
|
||||
- 2026-06-15: Initial decision draft generated from accepted `AGD-0043`.
|
||||
- 2026-06-20: Attached `PLN-0122` to tighten the publication-store implementation without changing the accepted worker/host presentation split.
|
||||
|
||||
@ -0,0 +1,128 @@
|
||||
---
|
||||
id: PLN-0122
|
||||
ticket: real-render-worker-establishment
|
||||
title: Unify Render Worker Publication Store
|
||||
status: done
|
||||
created: 2026-06-20
|
||||
completed: 2026-06-20
|
||||
ref_decisions: [DEC-0033]
|
||||
tags: [runtime, renderer, worker, host, publication]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Make worker frame publication have one observable authority by moving latest published frame storage behind the `RenderWorkerFrameSink` implementation used by the desktop host.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0033` requires the worker to publish owned `OwnedRgba8888Frame` values while the host event loop remains responsible for native upload and present. The current desktop integration starts the worker with a host sink that accepts frames but discards them, while host presentation reads the latest frame from `RenderWorkerController`.
|
||||
|
||||
That works mechanically, but it weakens the publication model: the sink is not the observable publication boundary, and the controller becomes both lifecycle owner and frame store. This plan tightens the implementation without changing the DEC-0033 architecture.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- Introduce an explicit latest-frame publication store that implements `RenderWorkerFrameSink`.
|
||||
- Make the desktop host pass that store to the worker and read frames from the same store for upload/present.
|
||||
- Keep native upload and present in the host event loop.
|
||||
- Keep `RenderWorkerController` responsible for worker lifecycle, handoff, shutdown, typed errors, and telemetry.
|
||||
- Preserve repeat-latest-frame semantics using the stored `OwnedRgba8888Frame`.
|
||||
- Add tests proving the sink/store is the observable publication authority.
|
||||
|
||||
### Excluded
|
||||
|
||||
- No change to worker scheduling, latest-wins handoff, or AppMode render policy.
|
||||
- No change to `OwnedRgba8888Frame` layout or pixel format.
|
||||
- No native window, pixels, winit, SDL, swapchain, or texture dependency inside worker/runtime crates.
|
||||
- No move of native present into the worker thread.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Add latest-frame store contract
|
||||
|
||||
**What:** Add a small thread-safe store for the latest published `OwnedRgba8888Frame`.
|
||||
|
||||
**How:** Implement a `RenderWorkerFrameSink` storage type using a lock-protected `Option<OwnedRgba8888Frame>`. It must provide read APIs for `latest_frame()` and `repeat_latest_frame()` or equivalent names that make host presentation read from the same publication authority used by `publish()`.
|
||||
|
||||
**File(s):** `crates/console/prometeu-hal/src/render_worker.rs` or `crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs`, depending on whether the store is shared as a HAL utility or kept runtime/host integration-local.
|
||||
|
||||
### Step 2 - Remove controller-owned latest frame storage
|
||||
|
||||
**What:** Stop treating `RenderWorkerController` as the frame publication store.
|
||||
|
||||
**How:** Remove or narrow `latest_published_frame` from controller state. `record_published` should update telemetry and rely on the sink/store for frame retention. Controller APIs that expose latest frames should either be removed or delegated explicitly to the publication store so there is not a second authoritative copy.
|
||||
|
||||
**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs`, `crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs`, `crates/console/prometeu-system/src/os/facades/vm.rs`.
|
||||
|
||||
### Step 3 - Wire desktop host to the publication store
|
||||
|
||||
**What:** Replace the no-op desktop sink with a real store-backed sink.
|
||||
|
||||
**How:** Add the store as host runner state, pass a clone to `start_render_worker`, and update redraw/presentation logic to read latest/repeated frames from that store. Delete the sink implementation that discards frames.
|
||||
|
||||
**File(s):** `crates/host/prometeu-host-desktop-winit/src/runner.rs`.
|
||||
|
||||
### Step 4 - Preserve host presentation split
|
||||
|
||||
**What:** Keep upload/present in the winit event loop while publication remains worker-owned.
|
||||
|
||||
**How:** Continue copying RGBA8888 pixels into the existing `pixels` frame only from `WindowEvent::RedrawRequested`. The store must contain owned memory only and must not expose native host resources to worker/runtime code.
|
||||
|
||||
**File(s):** `crates/host/prometeu-host-desktop-winit/src/runner.rs`, `crates/host/prometeu-host-desktop-winit/src/utilities.rs` if helper changes are needed.
|
||||
|
||||
### Step 5 - Add focused tests
|
||||
|
||||
**What:** Prove one-publication-authority behavior.
|
||||
|
||||
**How:** Add tests that publish through the sink and read the same frame through the store, prove repeat uses the stored frame without rendering again, and prove desktop presentation state observes store publication rather than controller-private storage.
|
||||
|
||||
**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/render_worker.rs`, `crates/host/prometeu-host-desktop-winit/src/runner.rs`, and any new store module tests.
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Store publishes and returns the latest `OwnedRgba8888Frame`.
|
||||
- Multiple publishes keep only the newest frame observable.
|
||||
- Repeat returns the stored frame and records repeat telemetry through the existing runtime path.
|
||||
- Controller publication telemetry remains correct after frame retention moves to the sink/store.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- `cargo test -p prometeu-system`
|
||||
- `cargo test -p prometeu-host-desktop-winit`
|
||||
- `cargo test -p prometeu-hal` if the store lives in HAL.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Run the desktop host with a simple game cartridge and verify the host presents worker-produced frames.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Desktop host no longer uses a no-op `RenderWorkerFrameSink` that discards frames.
|
||||
- [x] The same sink/store that receives `publish(frame)` is the source used by host presentation.
|
||||
- [x] `RenderWorkerController` no longer owns an independent authoritative latest-frame store.
|
||||
- [x] Worker/runtime crates remain free of native window and presentation APIs.
|
||||
- [x] Existing worker lifecycle, shutdown, stale epoch, repeat, and telemetry tests pass.
|
||||
- [x] Focused host tests prove the real publication store drives redraw/presentation.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `DEC-0033` remains the governing decision.
|
||||
- `PLN-0118` repeat/stale behavior and `PLN-0119` desktop presentation integration are the implementation context being tightened.
|
||||
|
||||
## Risks
|
||||
|
||||
- Moving frame retention out of the controller can accidentally split repeat telemetry from host presentation; keep repeat accounting in one explicit runtime path.
|
||||
- Placing the store in HAL may overexpose a host convenience type; placing it in system may require a narrower host-facing API. Choose the smallest layer that avoids circular dependencies and keeps DEC-0033 boundaries intact.
|
||||
- Holding a mutex while copying pixels into the host frame would increase present latency; clone or take the owned frame before doing host upload work.
|
||||
|
||||
## Validation Evidence
|
||||
|
||||
- `cargo fmt --check`: passed.
|
||||
- `cargo test -p prometeu-system -p prometeu-host-desktop-winit`: passed.
|
||||
- `cargo test --workspace`: passed.
|
||||
- Publication residue scan: no `HostWorkerFrameSink`, `latest_render_worker_frame`, `repeat_latest_render_worker_frame`, controller `latest_published_frame` storage API, or `repeat_latest_frame` API remains in production crates.
|
||||
- Native presentation dependency scan: worker/runtime console code remains free of native window/presentation APIs; matches containing `pixels` are HAL pixel field names, not `pixels`/winit host APIs.
|
||||
- Manual desktop window run was not performed in this environment; `host_presentation_observes_worker_publication_store` covers the host publication/redraw boundary without native window setup.
|
||||
Loading…
x
Reference in New Issue
Block a user