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::vm_runtime::{
RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff, RenderWorkerHandoffWait,
VirtualMachineRuntime,
RenderWorkerOwnership, VirtualMachineRuntime,
};
pub use services::windows;

View File

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

View File

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

View File

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

View File

@ -72,7 +72,14 @@ impl FakeRenderBackend {
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) {

View File

@ -246,6 +246,7 @@ impl VirtualMachineRuntime {
self.current_cartridge_app_mode,
self.current_app_id,
);
self.sync_render_worker_ownership();
if self.current_cartridge_app_mode == AppMode::Game {
let mut packet = platform.game2d_frame_composer().close_game2d_packet();
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
ticket: real-render-worker-establishment
title: Add Stale Epoch and Repeat Frame Behavior
status: open
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]