implements PLN-0114

This commit is contained in:
bQUARKz 2026-06-15 16:11:34 +01:00
parent 76d4e9c8ce
commit 28105c2bf2
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
6 changed files with 179 additions and 6 deletions

View File

@ -6,6 +6,8 @@ mod lifecycle;
pub mod render_manager; pub mod render_manager;
pub mod render_worker_handoff; pub mod render_worker_handoff;
#[cfg(test)] #[cfg(test)]
pub(crate) mod render_worker_test_harness;
#[cfg(test)]
mod tests; mod tests;
mod tick; mod tick;

View File

@ -122,7 +122,7 @@ mod tests {
} }
#[test] #[test]
fn render_worker_handoff_waits_for_publish_without_sleep() { fn render_worker_handoff_waits_for_publish_without_timing_delay() {
let handoff = Arc::new(RenderWorkerHandoff::default()); let handoff = Arc::new(RenderWorkerHandoff::default());
let consumer_handoff = Arc::clone(&handoff); let consumer_handoff = Arc::clone(&handoff);
let (waiting_tx, waiting_rx) = mpsc::channel(); let (waiting_tx, waiting_rx) = mpsc::channel();

View File

@ -0,0 +1,166 @@
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::{
FrameId, Game2DFramePacket, OwnedRgba8888Frame, RenderOwnership, RenderSubmission,
RenderWorkerError, ShellUiFramePacket,
};
use std::sync::{Arc, Condvar, Mutex};
#[derive(Debug, Default)]
struct RenderGateState {
entered: u64,
released: u64,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct RenderGate {
state: Arc<(Mutex<RenderGateState>, Condvar)>,
}
impl RenderGate {
pub(crate) fn block_until_released(&self) {
let (lock, condvar) = &*self.state;
let mut state = lock.lock().unwrap();
state.entered = state.entered.wrapping_add(1);
let entry = state.entered;
condvar.notify_all();
while state.released < entry {
state = condvar.wait(state).unwrap();
}
}
pub(crate) fn wait_for_entries(&self, count: u64) {
let (lock, condvar) = &*self.state;
let mut state = lock.lock().unwrap();
while state.entered < count {
state = condvar.wait(state).unwrap();
}
}
pub(crate) fn release_one(&self) {
let (lock, condvar) = &*self.state;
let mut state = lock.lock().unwrap();
state.released = state.released.wrapping_add(1);
condvar.notify_all();
}
}
#[derive(Debug, Default)]
pub(crate) struct FakeRenderBackend {
gate: Option<RenderGate>,
next_error: Mutex<Option<RenderWorkerError>>,
published_frames: Mutex<Vec<OwnedRgba8888Frame>>,
}
impl FakeRenderBackend {
pub(crate) fn with_gate(gate: RenderGate) -> Self {
Self { gate: Some(gate), ..Default::default() }
}
pub(crate) fn fail_next(&self, error: RenderWorkerError) {
*self.next_error.lock().unwrap() = Some(error);
}
pub(crate) fn render(
&self,
submission: &RenderSubmission,
) -> Result<OwnedRgba8888Frame, RenderWorkerError> {
if let Some(gate) = &self.gate {
gate.block_until_released();
}
if let Some(error) = self.next_error.lock().unwrap().take() {
return Err(error);
}
Ok(owned_frame(submission.frame_id, 0xff00_0000 | submission.frame_id.get() as u32))
}
pub(crate) fn publish(&self, frame: OwnedRgba8888Frame) {
self.published_frames.lock().unwrap().push(frame);
}
pub(crate) fn published_frames(&self) -> Vec<OwnedRgba8888Frame> {
self.published_frames.lock().unwrap().clone()
}
}
pub(crate) fn game_submission(frame_id: u64, epoch: u64, app_id: u32) -> RenderSubmission {
RenderSubmission::game2d(FrameId::new(frame_id), Game2DFramePacket::default())
.with_ownership(RenderOwnership::new(epoch, AppMode::Game, app_id))
}
pub(crate) fn shell_submission(frame_id: u64, epoch: u64, app_id: u32) -> RenderSubmission {
RenderSubmission::shell_ui(FrameId::new(frame_id), ShellUiFramePacket::new(Vec::new()))
.with_ownership(RenderOwnership::new(epoch, AppMode::Shell, app_id))
}
pub(crate) fn owned_frame(frame_id: FrameId, pixel: u32) -> OwnedRgba8888Frame {
OwnedRgba8888Frame::packed(
frame_id,
RenderOwnership::new(0, AppMode::Game, 0),
1,
1,
vec![pixel],
)
.expect("test frame should be valid")
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
use std::thread;
#[test]
fn render_worker_harness_blocks_and_releases_render_without_timing_delay() {
let gate = RenderGate::default();
let backend = Arc::new(FakeRenderBackend::with_gate(gate.clone()));
let render_backend = Arc::clone(&backend);
let submission = game_submission(4, 1, 9);
let (done_tx, done_rx) = mpsc::channel();
let worker = thread::spawn(move || {
let frame = render_backend.render(&submission).expect("render should succeed");
done_tx.send(frame.frame_id).unwrap();
});
gate.wait_for_entries(1);
assert!(done_rx.try_recv().is_err());
gate.release_one();
assert_eq!(done_rx.recv().expect("render should finish"), FrameId::new(4));
worker.join().expect("worker thread should finish");
}
#[test]
fn render_worker_harness_records_published_owned_frames() {
let backend = FakeRenderBackend::default();
let frame = owned_frame(FrameId::new(8), 0xff00_ff00);
backend.publish(frame.clone());
assert_eq!(backend.published_frames(), vec![frame]);
}
#[test]
fn render_worker_harness_injects_backend_errors() {
let backend = FakeRenderBackend::default();
backend.fail_next(RenderWorkerError::RenderFailed);
let error = backend.render(&game_submission(5, 1, 1)).expect_err("render should fail");
assert_eq!(error, RenderWorkerError::RenderFailed);
assert!(backend.render(&game_submission(6, 1, 1)).is_ok());
}
#[test]
fn render_worker_harness_builds_game_and_shell_submissions() {
let game = game_submission(1, 2, 3);
let shell = shell_submission(4, 5, 6);
assert_eq!(game.frame_id, FrameId::new(1));
assert_eq!(game.ownership, RenderOwnership::new(2, AppMode::Game, 3));
assert_eq!(shell.frame_id, FrameId::new(4));
assert_eq!(shell.ownership, RenderOwnership::new(5, AppMode::Shell, 6));
}
}

View File

@ -391,15 +391,20 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
loop { let mut asset_ready = false;
for _ in 0..1_000 {
match platform.local_hardware().assets.status(handle) { match platform.local_hardware().assets.status(handle) {
LoadStatus::READY => break, LoadStatus::READY => {
asset_ready = true;
break;
}
LoadStatus::PENDING | LoadStatus::LOADING => { LoadStatus::PENDING | LoadStatus::LOADING => {
std::thread::sleep(std::time::Duration::from_millis(1)); std::thread::yield_now();
} }
other => panic!("unexpected asset status before commit: {:?}", other), other => panic!("unexpected asset status before commit: {:?}", other),
} }
} }
assert!(asset_ready, "asset did not become ready before commit");
assert_eq!(platform.local_hardware().assets.commit(handle), AssetOpStatus::Ok); assert_eq!(platform.local_hardware().assets.commit(handle), AssetOpStatus::Ok);
platform.local_hardware().assets.apply_commits(); platform.local_hardware().assets.apply_commits();

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
id: PLN-0114 id: PLN-0114
ticket: real-render-worker-establishment ticket: real-render-worker-establishment
title: Add Deterministic Render Worker Test Harness title: Add Deterministic Render Worker Test Harness
status: open status: done
created: 2026-06-15 created: 2026-06-15
completed: completed:
ref_decisions: [DEC-0033] ref_decisions: [DEC-0033]