implements PLN-0118

This commit is contained in:
bQUARKz 2026-06-16 05:38:02 +01:00
parent cfb5b1ac89
commit 6b5b27d068
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
8 changed files with 147 additions and 13 deletions

View File

@ -12,6 +12,6 @@ pub use services::process;
pub use services::task; pub use services::task;
pub use services::vm_runtime::{ pub use services::vm_runtime::{
RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff, RenderWorkerHandoffWait, RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff, RenderWorkerHandoffWait,
VirtualMachineRuntime, RenderWorkerOwnership, VirtualMachineRuntime,
}; };
pub use services::windows; pub use services::windows;

View File

@ -27,6 +27,7 @@ impl VirtualMachineRuntime {
render_capabilities: RenderRuntimeCapabilities::default(), render_capabilities: RenderRuntimeCapabilities::default(),
render_manager: RenderManager::new(AppMode::Game), render_manager: RenderManager::new(AppMode::Game),
render_worker_handoff: Arc::new(RenderWorkerHandoff::default()), render_worker_handoff: Arc::new(RenderWorkerHandoff::default()),
render_worker_ownership: RenderWorkerOwnership::default(),
render_worker_controller: None, render_worker_controller: None,
gfx2d_commands: Vec::new(), gfx2d_commands: Vec::new(),
gfxui_commands: Vec::new(), gfxui_commands: Vec::new(),
@ -129,6 +130,8 @@ impl VirtualMachineRuntime {
self.render_manager = RenderManager::new(AppMode::Game); self.render_manager = RenderManager::new(AppMode::Game);
let _ = self.stop_render_worker(); let _ = self.stop_render_worker();
self.render_worker_handoff = Arc::new(RenderWorkerHandoff::default()); self.render_worker_handoff = Arc::new(RenderWorkerHandoff::default());
self.render_worker_ownership =
RenderWorkerOwnership::new(self.render_manager.active_ownership());
self.gfx2d_commands.clear(); self.gfx2d_commands.clear();
self.gfxui_commands.clear(); self.gfxui_commands.clear();
self.logs_written_this_frame.clear(); self.logs_written_this_frame.clear();
@ -159,9 +162,11 @@ impl VirtualMachineRuntime {
{ {
let _ = self.stop_render_worker(); let _ = self.stop_render_worker();
self.render_worker_handoff = Arc::new(RenderWorkerHandoff::default()); 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( self.render_worker_controller = Some(RenderWorkerController::start(
config, config,
Arc::clone(&self.render_worker_handoff), Arc::clone(&self.render_worker_handoff),
self.render_worker_ownership.clone(),
backend, backend,
sink, sink,
)); ));
@ -184,6 +189,10 @@ impl VirtualMachineRuntime {
true true
} }
pub(crate) fn sync_render_worker_ownership(&self) {
self.render_worker_ownership.set(self.render_manager.active_ownership());
}
pub fn initialize_vm( pub fn initialize_vm(
&mut self, &mut self,
log_service: &mut LogService, log_service: &mut LogService,

View File

@ -21,7 +21,7 @@ use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier};
use prometeu_hal::{Gfx2dCommand, GfxUiCommand}; use prometeu_hal::{Gfx2dCommand, GfxUiCommand};
use prometeu_vm::VirtualMachine; use prometeu_vm::VirtualMachine;
pub use render_manager::{RenderManager, RenderRuntimeCapabilities}; 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}; pub use render_worker_handoff::{RenderWorkerHandoff, RenderWorkerHandoffWait};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@ -42,6 +42,7 @@ pub struct VirtualMachineRuntime {
pub render_capabilities: RenderRuntimeCapabilities, pub render_capabilities: RenderRuntimeCapabilities,
pub render_manager: RenderManager, pub render_manager: RenderManager,
pub render_worker_handoff: Arc<RenderWorkerHandoff>, pub render_worker_handoff: Arc<RenderWorkerHandoff>,
pub render_worker_ownership: RenderWorkerOwnership,
pub render_worker_controller: Option<RenderWorkerController>, pub render_worker_controller: Option<RenderWorkerController>,
pub gfx2d_commands: Vec<Gfx2dCommand>, pub gfx2d_commands: Vec<Gfx2dCommand>,
pub gfxui_commands: Vec<GfxUiCommand>, pub gfxui_commands: Vec<GfxUiCommand>,

View File

@ -1,6 +1,8 @@
use crate::services::vm_runtime::{RenderWorkerHandoff, RenderWorkerHandoffWait}; use crate::services::vm_runtime::{RenderWorkerHandoff, RenderWorkerHandoffWait};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::{ use prometeu_hal::{
FrameId, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, RenderWorkerTelemetry, FrameId, OwnedRgba8888Frame, RenderOwnership, RenderWorkerBackend, RenderWorkerError,
RenderWorkerFrameSink, RenderWorkerTelemetry,
}; };
use std::panic::{AssertUnwindSafe, catch_unwind}; use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError}; use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
@ -23,12 +25,39 @@ impl Default for RenderWorkerConfig {
struct RenderWorkerState { struct RenderWorkerState {
telemetry: RenderWorkerTelemetry, telemetry: RenderWorkerTelemetry,
last_error: Option<RenderWorkerError>, last_error: Option<RenderWorkerError>,
latest_published_frame: Option<OwnedRgba8888Frame>,
}
#[derive(Clone, Debug)]
pub struct RenderWorkerOwnership {
active: Arc<Mutex<RenderOwnership>>,
}
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)] #[derive(Debug)]
pub struct RenderWorkerController { pub struct RenderWorkerController {
config: RenderWorkerConfig, config: RenderWorkerConfig,
handoff: Arc<RenderWorkerHandoff>, handoff: Arc<RenderWorkerHandoff>,
ownership: RenderWorkerOwnership,
state: Arc<Mutex<RenderWorkerState>>, state: Arc<Mutex<RenderWorkerState>>,
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
done_rx: Receiver<Result<(), RenderWorkerError>>, done_rx: Receiver<Result<(), RenderWorkerError>>,
@ -38,6 +67,7 @@ impl RenderWorkerController {
pub fn start<B, S>( pub fn start<B, S>(
config: RenderWorkerConfig, config: RenderWorkerConfig,
handoff: Arc<RenderWorkerHandoff>, handoff: Arc<RenderWorkerHandoff>,
ownership: RenderWorkerOwnership,
backend: B, backend: B,
sink: S, sink: S,
) -> Self ) -> Self
@ -47,13 +77,14 @@ impl RenderWorkerController {
{ {
let state = Arc::new(Mutex::new(RenderWorkerState::default())); let state = Arc::new(Mutex::new(RenderWorkerState::default()));
let worker_handoff = Arc::clone(&handoff); let worker_handoff = Arc::clone(&handoff);
let worker_ownership = ownership.clone();
let worker_state = Arc::clone(&state); let worker_state = Arc::clone(&state);
let panic_state = Arc::clone(&state); let panic_state = Arc::clone(&state);
let (done_tx, done_rx) = mpsc::channel(); let (done_tx, done_rx) = mpsc::channel();
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
let result = catch_unwind(AssertUnwindSafe(|| { 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); .map_err(|_| RenderWorkerError::WorkerPanic);
if let Err(error) = result { if let Err(error) = result {
@ -62,7 +93,7 @@ impl RenderWorkerController {
let _ = done_tx.send(result); 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> { pub fn stop(&mut self) -> Result<(), RenderWorkerError> {
@ -95,8 +126,12 @@ impl RenderWorkerController {
telemetry.render_failures = state.telemetry.render_failures; telemetry.render_failures = state.telemetry.render_failures;
telemetry.publish_failures = state.telemetry.publish_failures; telemetry.publish_failures = state.telemetry.publish_failures;
telemetry.present_failures = state.telemetry.present_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.shutdown_timeouts = state.telemetry.shutdown_timeouts;
telemetry.worker_panics = state.telemetry.worker_panics; 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_published_frame_id = state.telemetry.last_published_frame_id;
telemetry.last_error_frame_id = state.telemetry.last_error_frame_id; telemetry.last_error_frame_id = state.telemetry.last_error_frame_id;
telemetry telemetry
@ -105,6 +140,21 @@ impl RenderWorkerController {
pub fn last_error(&self) -> Option<RenderWorkerError> { pub fn last_error(&self) -> Option<RenderWorkerError> {
self.state.lock().unwrap().last_error self.state.lock().unwrap().last_error
} }
pub fn active_ownership(&self) -> RenderOwnership {
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> {
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 { impl Drop for RenderWorkerController {
@ -117,6 +167,7 @@ impl Drop for RenderWorkerController {
fn run_worker_loop<B, S>( fn run_worker_loop<B, S>(
handoff: Arc<RenderWorkerHandoff>, handoff: Arc<RenderWorkerHandoff>,
ownership: RenderWorkerOwnership,
state: Arc<Mutex<RenderWorkerState>>, state: Arc<Mutex<RenderWorkerState>>,
backend: B, backend: B,
sink: S, sink: S,
@ -141,17 +192,28 @@ fn run_worker_loop<B, S>(
} }
}; };
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); .map_err(|_| RenderWorkerError::WorkerPanic);
match publish_result { 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), Ok(Err(error)) | Err(error) => record_error(&state, error, frame_id),
} }
} }
} }
fn record_published(state: &Arc<Mutex<RenderWorkerState>>, frame_id: FrameId) { fn record_published(state: &Arc<Mutex<RenderWorkerState>>, frame: OwnedRgba8888Frame) {
state.lock().unwrap().telemetry.record_published(frame_id.get()); 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<Mutex<RenderWorkerState>>, frame_id: FrameId) {
state.lock().unwrap().telemetry.record_stale_submission_discarded(frame_id.get());
} }
fn record_error( fn record_error(
@ -171,7 +233,10 @@ mod tests {
use crate::services::vm_runtime::render_worker_test_harness::{ use crate::services::vm_runtime::render_worker_test_harness::{
FakeRenderBackend, RenderGate, game_submission, 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)] #[derive(Clone, Debug)]
struct SharedFakeBackend(Arc<FakeRenderBackend>); struct SharedFakeBackend(Arc<FakeRenderBackend>);
@ -220,6 +285,7 @@ mod tests {
let mut controller = RenderWorkerController::start( let mut controller = RenderWorkerController::start(
RenderWorkerConfig::default(), RenderWorkerConfig::default(),
Arc::clone(&handoff), Arc::clone(&handoff),
RenderWorkerOwnership::default(),
SharedFakeBackend(Arc::clone(&backend)), SharedFakeBackend(Arc::clone(&backend)),
SharedFakeBackend(backend), SharedFakeBackend(backend),
); );
@ -235,6 +301,7 @@ mod tests {
let mut controller = RenderWorkerController::start( let mut controller = RenderWorkerController::start(
RenderWorkerConfig::default(), RenderWorkerConfig::default(),
Arc::clone(&handoff), Arc::clone(&handoff),
RenderWorkerOwnership::new(RenderOwnership::new(1, AppMode::Game, 1)),
SharedFakeBackend(Arc::clone(&backend)), SharedFakeBackend(Arc::clone(&backend)),
SharedFakeBackend(Arc::clone(&backend)), SharedFakeBackend(Arc::clone(&backend)),
); );
@ -256,6 +323,7 @@ mod tests {
let mut controller = RenderWorkerController::start( let mut controller = RenderWorkerController::start(
RenderWorkerConfig { shutdown_timeout: Duration::from_millis(1) }, RenderWorkerConfig { shutdown_timeout: Duration::from_millis(1) },
Arc::clone(&handoff), Arc::clone(&handoff),
RenderWorkerOwnership::default(),
SharedFakeBackend(Arc::clone(&backend)), SharedFakeBackend(Arc::clone(&backend)),
SharedFakeBackend(Arc::clone(&backend)), SharedFakeBackend(Arc::clone(&backend)),
); );
@ -278,6 +346,7 @@ mod tests {
let mut controller = RenderWorkerController::start( let mut controller = RenderWorkerController::start(
RenderWorkerConfig::default(), RenderWorkerConfig::default(),
Arc::clone(&handoff), Arc::clone(&handoff),
RenderWorkerOwnership::default(),
PanickingBackend, PanickingBackend,
NoopSink, NoopSink,
); );
@ -290,6 +359,53 @@ mod tests {
assert_eq!(controller.telemetry().last_error_frame_id, 13); 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] #[test]
fn runtime_publishes_game_submissions_to_worker_without_waiting_for_render() { fn runtime_publishes_game_submissions_to_worker_without_waiting_for_render() {
let gate = RenderGate::default(); let gate = RenderGate::default();

View File

@ -72,7 +72,14 @@ impl FakeRenderBackend {
return Err(error); 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) { pub(crate) fn publish(&self, frame: OwnedRgba8888Frame) {

View File

@ -246,6 +246,7 @@ impl VirtualMachineRuntime {
self.current_cartridge_app_mode, self.current_cartridge_app_mode,
self.current_app_id, self.current_app_id,
); );
self.sync_render_worker_ownership();
if self.current_cartridge_app_mode == AppMode::Game { if self.current_cartridge_app_mode == AppMode::Game {
let mut packet = platform.game2d_frame_composer().close_game2d_packet(); let mut packet = platform.game2d_frame_composer().close_game2d_packet();
packet.gfx2d = std::mem::take(&mut self.gfx2d_commands); packet.gfx2d = std::mem::take(&mut self.gfx2d_commands);

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
id: PLN-0118 id: PLN-0118
ticket: real-render-worker-establishment ticket: real-render-worker-establishment
title: Add Stale Epoch and Repeat Frame Behavior title: Add Stale Epoch and Repeat Frame Behavior
status: open status: done
created: 2026-06-15 created: 2026-06-15
completed: completed:
ref_decisions: [DEC-0033] ref_decisions: [DEC-0033]