implements PLN-0114
This commit is contained in:
parent
76d4e9c8ce
commit
28105c2bf2
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
@ -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]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user