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_worker_handoff;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod render_worker_test_harness;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod tick;
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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 consumer_handoff = Arc::clone(&handoff);
|
||||
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");
|
||||
|
||||
loop {
|
||||
let mut asset_ready = false;
|
||||
for _ in 0..1_000 {
|
||||
match platform.local_hardware().assets.status(handle) {
|
||||
LoadStatus::READY => break,
|
||||
LoadStatus::READY => {
|
||||
asset_ready = true;
|
||||
break;
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
assert!(asset_ready, "asset did not become ready before commit");
|
||||
|
||||
assert_eq!(platform.local_hardware().assets.commit(handle), AssetOpStatus::Ok);
|
||||
platform.local_hardware().assets.apply_commits();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -2,7 +2,7 @@
|
||||
id: PLN-0114
|
||||
ticket: real-render-worker-establishment
|
||||
title: Add Deterministic Render Worker Test Harness
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-06-15
|
||||
completed:
|
||||
ref_decisions: [DEC-0033]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user