Compare commits

..

18 Commits

Author SHA1 Message Date
aa917cf44e
implements PLN-0121
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
2026-06-16 05:49:46 +01:00
850ca8691b
implements PLN-0120 2026-06-16 05:46:13 +01:00
67a629dbe4
implements PLN-0119 2026-06-16 05:44:08 +01:00
6b5b27d068
implements PLN-0118 2026-06-16 05:38:02 +01:00
cfb5b1ac89
implements PLN-0117 2026-06-15 16:20:46 +01:00
0c6a669e6e
implements PLN-0116 2026-06-15 16:17:16 +01:00
4b890850aa
implements PLN-0115 2026-06-15 16:13:56 +01:00
28105c2bf2
implements PLN-0114 2026-06-15 16:11:34 +01:00
76d4e9c8ce
implements PLN-0113 2026-06-15 16:09:39 +01:00
57bf2b84d3
implements PLN-0112 2026-06-15 16:06:53 +01:00
4c48b92f3b
implements PLN-0111 2026-06-15 16:04:36 +01:00
12d314a62d
implements PLN-0110 2026-06-15 16:01:47 +01:00
3916a652b2
add real render worker implementation plans 2026-06-15 15:49:17 +01:00
cafbc42018
accept real render worker decision 2026-06-15 12:31:01 +01:00
1022750c12
resolve render worker agenda questions 2026-06-15 12:28:25 +01:00
373f842bef
align render worker agenda direction 2026-06-15 12:15:57 +01:00
a761e73587
update real render worker agenda 2026-06-15 12:04:43 +01:00
b4f7b4f3c0
clean up debugger platform tests 2026-06-15 11:59:09 +01:00
54 changed files with 3261 additions and 89 deletions

View File

@ -33,6 +33,8 @@ use std::sync::Arc;
/// - **Audio**: Stereo, Command-based mixing.
/// - **Input**: 12-button Digital Gamepad + Absolute Touch/Mouse.
pub struct Hardware {
/// Shared physical memory bank ownership used by peripherals and worker backends.
pub memory_banks: Arc<MemoryBanks>,
/// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps.
pub gfx: Gfx,
/// Canonical frame orchestration owner for scene/camera/cache/resolver/sprites.
@ -216,6 +218,7 @@ impl Hardware {
);
let glyph_slot_index = assets.glyph_asset_slot_index();
Self {
memory_banks: Arc::clone(&memory_banks),
gfx: Gfx::new(
Self::W,
Self::H,

View File

@ -3,6 +3,7 @@ mod audio;
mod frame_composer;
mod gfx;
pub mod hardware;
mod local_render_backend;
mod memory_banks;
mod pad;
mod test_platform;
@ -12,9 +13,10 @@ pub use crate::asset::AssetManager;
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
pub use crate::frame_composer::{ComposerBuffer, FrameComposer, SceneStatus, SpriteController};
pub use crate::gfx::Gfx;
pub use crate::local_render_backend::LocalFramebufferRenderBackend;
pub use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, RenderResourceAccess,
SceneBankPoolAccess, SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
};
pub use crate::pad::Pad;
pub use crate::test_platform::TestPlatform;

View File

@ -0,0 +1,162 @@
use crate::asset::GlyphAssetSlotIndex;
use crate::frame_composer::FrameComposer;
use crate::gfx::Gfx;
use crate::memory_banks::{GlyphBankPoolAccess, MemoryBanks, SceneBankPoolAccess};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::{
OwnedRgba8888Frame, RenderSubmission, RenderSubmissionPacket, RenderWorkerBackend,
RenderWorkerError,
};
use std::sync::{Arc, Mutex};
pub struct LocalFramebufferRenderBackend {
gfx: Mutex<Gfx>,
frame_composer: Mutex<FrameComposer>,
}
impl LocalFramebufferRenderBackend {
pub fn new(width: usize, height: usize, glyph_banks: Arc<dyn GlyphBankPoolAccess>) -> Self {
Self::new_with_parts(
width,
height,
glyph_banks,
Arc::new(MemoryBanks::new()),
GlyphAssetSlotIndex::new(),
)
}
pub fn new_with_memory_banks(
width: usize,
height: usize,
memory_banks: Arc<MemoryBanks>,
glyph_slot_index: GlyphAssetSlotIndex,
) -> Self {
Self::new_with_parts(
width,
height,
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolAccess>,
memory_banks,
glyph_slot_index,
)
}
fn new_with_parts(
width: usize,
height: usize,
glyph_banks: Arc<dyn GlyphBankPoolAccess>,
scene_banks: Arc<dyn SceneBankPoolAccess>,
glyph_slot_index: GlyphAssetSlotIndex,
) -> Self {
Self {
gfx: Mutex::new(Gfx::new(width, height, glyph_banks)),
frame_composer: Mutex::new(FrameComposer::new(
width,
height,
scene_banks,
glyph_slot_index,
)),
}
}
pub fn render_submission(
&self,
submission: &RenderSubmission,
) -> Result<OwnedRgba8888Frame, RenderWorkerError> {
let mut gfx = self.gfx.lock().map_err(|_| RenderWorkerError::InternalFailure)?;
match &submission.packet {
RenderSubmissionPacket::Game2D(packet) if submission.app_mode == AppMode::Game => {
if packet.composer.bound_scene.is_none() {
gfx.render_game2d_frame_packet(packet);
} else {
let mut frame_composer = self
.frame_composer
.lock()
.map_err(|_| RenderWorkerError::InternalFailure)?;
frame_composer.render_game2d_frame_packet(packet, &mut *gfx);
gfx.apply_gfx2d_commands(&packet.gfx2d);
}
}
RenderSubmissionPacket::ShellUi(packet) if submission.app_mode == AppMode::Shell => {
gfx.render_shell_ui_frame_packet(packet);
}
_ => return Err(RenderWorkerError::RenderFailed),
}
gfx.present();
let (width, height) = gfx.size();
OwnedRgba8888Frame::packed(
submission.frame_id,
submission.ownership,
width,
height,
gfx.front_buffer().to_vec(),
)
.map_err(|_| RenderWorkerError::InternalFailure)
}
}
impl RenderWorkerBackend for LocalFramebufferRenderBackend {
fn render(
&self,
submission: &RenderSubmission,
) -> Result<OwnedRgba8888Frame, RenderWorkerError> {
self.render_submission(submission)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_banks::MemoryBanks;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::color::Color;
use prometeu_hal::primitives::Rect;
use prometeu_hal::{
FrameId, Game2DFramePacket, Gfx2dCommand, GfxUiCommand, RenderOwnership, ShellUiFramePacket,
};
fn backend() -> LocalFramebufferRenderBackend {
let banks = Arc::new(MemoryBanks::new());
LocalFramebufferRenderBackend::new(4, 4, banks)
}
#[test]
fn local_backend_renders_game2d_submission_to_owned_frame() {
let backend = backend();
let mut packet = Game2DFramePacket::default();
packet.gfx2d = vec![Gfx2dCommand::FillRect {
rect: Rect { x: 1, y: 1, w: 2, h: 2 },
color: Color::RED,
}];
let ownership = RenderOwnership::new(3, AppMode::Game, 9);
let submission =
RenderSubmission::game2d(FrameId::new(5), packet).with_ownership(ownership);
let frame = backend.render(&submission).expect("game frame should render");
assert_eq!(frame.frame_id, FrameId::new(5));
assert_eq!(frame.ownership, ownership);
assert_eq!(frame.width, 4);
assert_eq!(frame.height, 4);
assert_eq!(frame.pixels[1 + 4], Color::RED.raw());
assert_eq!(frame.pixels[2 + 2 * 4], Color::RED.raw());
}
#[test]
fn local_backend_renders_shell_ui_submission_to_owned_frame() {
let backend = backend();
let packet = ShellUiFramePacket::new(vec![GfxUiCommand::FillRect {
rect: Rect { x: 0, y: 0, w: 1, h: 1 },
color: Color::GREEN,
}]);
let ownership = RenderOwnership::new(4, AppMode::Shell, 10);
let submission =
RenderSubmission::shell_ui(FrameId::new(6), packet).with_ownership(ownership);
let frame = backend.render(&submission).expect("shell frame should render");
assert_eq!(frame.frame_id, FrameId::new(6));
assert_eq!(frame.ownership, ownership);
assert_eq!(frame.pixels[0], Color::GREEN.raw());
}
}

View File

@ -2,11 +2,12 @@
//!
//! Render consumers must cross this boundary through small, read-only traits.
//! Frame packets carry resource IDs; large resident resources stay in banks and
//! are returned as `Arc` snapshots of the slot contents. Installation and slot
//! replacement remain separate owner-side operations and are not part of the
//! render read surface.
//! are read in place through a narrow HAL callback contract. Installation and
//! slot replacement remain separate owner-side operations and are not part of
//! the render read surface.
use prometeu_hal::glyph_bank::GlyphBank;
use prometeu_hal::render_resources::RenderResourceAccess as HalRenderResourceAccess;
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::sound_bank::SoundBank;
use std::sync::{Arc, RwLock};
@ -53,15 +54,6 @@ pub trait SceneBankPoolInstaller: Send + Sync {
fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>);
}
/// Read-only resource surface needed by render consumers.
///
/// This is intentionally narrower than `MemoryBanks`: render code can resolve
/// stable packet IDs to resident glyph/scene banks, but cannot install or swap
/// resources through this trait.
pub trait RenderResourceAccess: GlyphBankPoolAccess + SceneBankPoolAccess {}
impl<T> RenderResourceAccess for T where T: GlyphBankPoolAccess + SceneBankPoolAccess {}
/// Centralized container for all hardware memory banks.
///
/// MemoryBanks represent the actual hardware slot state.
@ -150,6 +142,28 @@ impl SceneBankPoolInstaller for MemoryBanks {
}
}
impl HalRenderResourceAccess for MemoryBanks {
fn with_glyph_bank<R>(&self, bank_id: usize, read: impl FnOnce(&GlyphBank) -> R) -> Option<R> {
let pool = self.glyph_bank_pool.read().unwrap();
let bank = pool.get(bank_id)?.as_deref()?;
Some(read(bank))
}
fn glyph_bank_count(&self) -> usize {
16
}
fn with_scene_bank<R>(&self, bank_id: usize, read: impl FnOnce(&SceneBank) -> R) -> Option<R> {
let pool = self.scene_bank_pool.read().unwrap();
let bank = pool.get(bank_id)?.as_deref()?;
Some(read(bank))
}
fn scene_bank_count(&self) -> usize {
16
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -193,24 +207,24 @@ mod tests {
assert_send_sync::<MemoryBanks>();
assert_send_sync::<Arc<dyn GlyphBankPoolAccess>>();
assert_send_sync::<Arc<dyn SceneBankPoolAccess>>();
assert_send_sync::<Arc<dyn RenderResourceAccess>>();
}
#[test]
fn render_resource_access_exposes_read_only_glyph_and_scene_banks() {
let banks = Arc::new(MemoryBanks::new());
fn render_resource_access_exposes_read_only_glyph_and_scene_banks_without_arc_contract() {
let banks = MemoryBanks::new();
banks.install_glyph_bank(0, Arc::new(make_glyph_bank()));
banks.install_scene_bank(1, Arc::new(make_scene_bank()));
let render_resources: Arc<dyn RenderResourceAccess> = banks;
let glyph_bank =
render_resources.glyph_bank_slot(0).expect("glyph bank slot should be readable");
let scene_bank =
render_resources.scene_bank_slot(1).expect("scene bank slot should be readable");
let glyph_tile_size =
HalRenderResourceAccess::with_glyph_bank(&banks, 0, |bank| bank.tile_size);
let scene_tile_size =
HalRenderResourceAccess::with_scene_bank(&banks, 1, |bank| bank.layers[0].tile_size);
assert_eq!(glyph_bank.tile_size, TileSize::Size8);
assert_eq!(scene_bank.layers[0].tile_size, TileSize::Size8);
assert!(render_resources.glyph_bank_slot(15).is_none());
assert!(render_resources.scene_bank_slot(15).is_none());
assert_eq!(glyph_tile_size, Some(TileSize::Size8));
assert_eq!(scene_tile_size, Some(TileSize::Size8));
assert_eq!(HalRenderResourceAccess::with_glyph_bank(&banks, 15, |_| ()), None);
assert_eq!(HalRenderResourceAccess::with_scene_bank(&banks, 15, |_| ()), None);
assert_eq!(HalRenderResourceAccess::glyph_bank_count(&banks), 16);
assert_eq!(HalRenderResourceAccess::scene_bank_count(&banks), 16);
}
}

View File

@ -17,10 +17,13 @@ pub mod input_signals;
pub mod log;
pub mod native_helpers;
pub mod native_interface;
pub mod owned_frame;
pub mod pad_bridge;
pub mod platform;
pub mod primitives;
pub mod render_resources;
pub mod render_submission;
pub mod render_worker;
pub mod sample;
pub mod scene_bank;
pub mod scene_layer;
@ -44,15 +47,20 @@ pub use host_return::HostReturn;
pub use input_signals::InputSignals;
pub use native_helpers::{expect_bool, expect_int};
pub use native_interface::{NativeInterface, SyscallId};
pub use owned_frame::{OwnedRgba8888Frame, OwnedRgba8888FrameError};
pub use pad_bridge::PadBridge;
pub use platform::{
AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform,
NoopTelemetryPlatform, RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform,
TelemetryPlatform, TestPlatform,
};
pub use render_resources::RenderResourceAccess;
pub use render_submission::{
BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket,
Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderOwnership, RenderSubmission,
RenderSubmissionPacket, ShellUiFramePacket,
};
pub use render_worker::{
RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, RenderWorkerTelemetry,
};
pub use touch_bridge::TouchBridge;

View File

@ -0,0 +1,127 @@
use crate::render_submission::{FrameId, RenderOwnership};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OwnedRgba8888Frame {
pub frame_id: FrameId,
pub ownership: RenderOwnership,
pub width: usize,
pub height: usize,
pub stride_pixels: usize,
pub pixels: Vec<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OwnedRgba8888FrameError {
EmptyDimensions,
StrideTooSmall,
PixelBufferTooSmall,
}
impl OwnedRgba8888Frame {
pub fn new(
frame_id: FrameId,
ownership: RenderOwnership,
width: usize,
height: usize,
stride_pixels: usize,
pixels: Vec<u32>,
) -> Result<Self, OwnedRgba8888FrameError> {
if width == 0 || height == 0 {
return Err(OwnedRgba8888FrameError::EmptyDimensions);
}
if stride_pixels < width {
return Err(OwnedRgba8888FrameError::StrideTooSmall);
}
let required_len = stride_pixels
.checked_mul(height)
.ok_or(OwnedRgba8888FrameError::PixelBufferTooSmall)?;
if pixels.len() < required_len {
return Err(OwnedRgba8888FrameError::PixelBufferTooSmall);
}
Ok(Self { frame_id, ownership, width, height, stride_pixels, pixels })
}
pub fn packed(
frame_id: FrameId,
ownership: RenderOwnership,
width: usize,
height: usize,
pixels: Vec<u32>,
) -> Result<Self, OwnedRgba8888FrameError> {
Self::new(frame_id, ownership, width, height, width, pixels)
}
pub fn required_pixel_len(&self) -> usize {
self.stride_pixels * self.height
}
pub fn row(&self, y: usize) -> Option<&[u32]> {
if y >= self.height {
return None;
}
let start = y * self.stride_pixels;
Some(&self.pixels[start..start + self.width])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app_mode::AppMode;
fn ownership() -> RenderOwnership {
RenderOwnership::new(11, AppMode::Game, 42)
}
#[test]
fn owned_rgba8888_frame_preserves_metadata_and_pixels() {
let pixels = vec![0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00];
let frame = OwnedRgba8888Frame::packed(FrameId::new(7), ownership(), 2, 2, pixels.clone())
.expect("valid packed RGBA8888 frame");
assert_eq!(frame.frame_id, FrameId::new(7));
assert_eq!(frame.ownership, ownership());
assert_eq!(frame.width, 2);
assert_eq!(frame.height, 2);
assert_eq!(frame.stride_pixels, 2);
assert_eq!(frame.pixels, pixels);
assert_eq!(frame.required_pixel_len(), 4);
}
#[test]
fn checked_constructor_rejects_empty_dimensions() {
let err = OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 0, 1, 1, vec![0])
.expect_err("zero width should be rejected");
assert_eq!(err, OwnedRgba8888FrameError::EmptyDimensions);
}
#[test]
fn checked_constructor_rejects_stride_smaller_than_width() {
let err = OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 3, 1, 2, vec![0, 0, 0])
.expect_err("stride smaller than width should be rejected");
assert_eq!(err, OwnedRgba8888FrameError::StrideTooSmall);
}
#[test]
fn checked_constructor_rejects_short_pixel_buffer() {
let err = OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 2, 2, 3, vec![0; 5])
.expect_err("buffer shorter than stride * height should be rejected");
assert_eq!(err, OwnedRgba8888FrameError::PixelBufferTooSmall);
}
#[test]
fn row_returns_visible_width_without_stride_padding() {
let frame =
OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 2, 2, 3, vec![1, 2, 99, 3, 4, 88])
.expect("valid padded frame");
assert_eq!(frame.row(0), Some(&[1, 2][..]));
assert_eq!(frame.row(1), Some(&[3, 4][..]));
assert_eq!(frame.row(2), None);
}
}

View File

@ -0,0 +1,109 @@
use crate::glyph_bank::GlyphBank;
use crate::scene_bank::SceneBank;
/// Read-only render resource lookup used by render backends.
///
/// Resource ids are stable bank slots carried by render submissions. The
/// callback shape keeps bank ownership inside the provider while exposing only
/// borrowed read access to the caller.
pub trait RenderResourceAccess {
/// Reads a resident glyph bank by id.
fn with_glyph_bank<R>(&self, bank_id: usize, read: impl FnOnce(&GlyphBank) -> R) -> Option<R>;
/// Returns the number of glyph bank ids accepted by this provider.
fn glyph_bank_count(&self) -> usize;
/// Reads a resident scene bank by id.
fn with_scene_bank<R>(&self, bank_id: usize, read: impl FnOnce(&SceneBank) -> R) -> Option<R>;
/// Returns the number of scene bank ids accepted by this provider.
fn scene_bank_count(&self) -> usize;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::glyph_bank::TileSize;
use crate::scene_layer::{ParallaxFactor, SceneLayer};
use crate::tile::Tile;
use crate::tilemap::TileMap;
struct TestRenderResources {
glyph_banks: Vec<Option<GlyphBank>>,
scene_banks: Vec<Option<SceneBank>>,
}
impl RenderResourceAccess for TestRenderResources {
fn with_glyph_bank<R>(
&self,
bank_id: usize,
read: impl FnOnce(&GlyphBank) -> R,
) -> Option<R> {
self.glyph_banks.get(bank_id)?.as_ref().map(read)
}
fn glyph_bank_count(&self) -> usize {
self.glyph_banks.len()
}
fn with_scene_bank<R>(
&self,
bank_id: usize,
read: impl FnOnce(&SceneBank) -> R,
) -> Option<R> {
self.scene_banks.get(bank_id)?.as_ref().map(read)
}
fn scene_bank_count(&self) -> usize {
self.scene_banks.len()
}
}
fn scene_bank(tile_size: TileSize) -> SceneBank {
let layer = SceneLayer {
active: true,
glyph_asset_id: 7,
tile_size,
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
tilemap: TileMap {
width: 1,
height: 1,
tiles: vec![Tile {
active: true,
glyph: Glyph { glyph_id: 3, palette_id: 0 },
flip_x: false,
flip_y: false,
}],
},
};
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
}
#[test]
fn reads_resident_resources_by_id() {
let resources = TestRenderResources {
glyph_banks: vec![None, Some(GlyphBank::new(TileSize::Size16, 16, 16)), None],
scene_banks: vec![Some(scene_bank(TileSize::Size8)), None],
};
assert_eq!(resources.glyph_bank_count(), 3);
assert_eq!(resources.scene_bank_count(), 2);
assert_eq!(resources.with_glyph_bank(1, |bank| bank.tile_size), Some(TileSize::Size16));
assert_eq!(
resources.with_scene_bank(0, |bank| bank.layers[0].tile_size),
Some(TileSize::Size8)
);
}
#[test]
fn missing_resources_return_none() {
let resources = TestRenderResources { glyph_banks: vec![None], scene_banks: vec![None] };
assert_eq!(resources.with_glyph_bank(0, |bank| bank.width), None);
assert_eq!(resources.with_glyph_bank(9, |bank| bank.width), None);
assert_eq!(resources.with_scene_bank(0, |bank| bank.layers[0].active), None);
assert_eq!(resources.with_scene_bank(9, |bank| bank.layers[0].active), None);
}
}

View File

@ -0,0 +1,203 @@
use std::fmt;
use crate::{OwnedRgba8888Frame, RenderSubmission};
/// Typed failure surface for the real render worker and its backend.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderWorkerError {
BackendUnavailable,
RenderFailed,
PublishFailed,
PresentFailed,
ShutdownTimeout,
StaleSubmissionDiscarded,
WorkerPanic,
InternalFailure,
}
impl fmt::Display for RenderWorkerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BackendUnavailable => f.write_str("render backend unavailable"),
Self::RenderFailed => f.write_str("render failed"),
Self::PublishFailed => f.write_str("render publish failed"),
Self::PresentFailed => f.write_str("render present failed"),
Self::ShutdownTimeout => f.write_str("render worker shutdown timed out"),
Self::StaleSubmissionDiscarded => f.write_str("stale render submission discarded"),
Self::WorkerPanic => f.write_str("render worker panicked"),
Self::InternalFailure => f.write_str("internal render worker failure"),
}
}
}
impl std::error::Error for RenderWorkerError {}
pub trait RenderWorkerBackend: Send + 'static {
fn render(
&self,
submission: &RenderSubmission,
) -> Result<OwnedRgba8888Frame, RenderWorkerError>;
}
pub trait RenderWorkerFrameSink: Send + 'static {
fn publish(&self, frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError>;
}
/// Snapshot of render worker handoff, render, publish, and shutdown counters.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RenderWorkerTelemetry {
pub produced_submissions: u64,
pub replaced_before_consume: u64,
pub consumed_submissions: u64,
pub published_frames: u64,
pub presented_frames: u64,
pub repeated_frames: u64,
pub stale_submission_discards: u64,
pub render_failures: u64,
pub publish_failures: u64,
pub present_failures: u64,
pub shutdown_timeouts: u64,
pub worker_panics: u64,
pub last_produced_frame_id: u64,
pub last_consumed_frame_id: u64,
pub last_published_frame_id: u64,
pub last_presented_frame_id: u64,
pub last_dropped_frame_id: u64,
pub last_error_frame_id: u64,
pub active_render_epoch: u64,
}
impl RenderWorkerTelemetry {
pub fn record_produced(&mut self, frame_id: u64) {
self.produced_submissions += 1;
self.last_produced_frame_id = frame_id;
}
pub fn record_replaced_before_consume(&mut self, frame_id: u64) {
self.replaced_before_consume += 1;
self.last_dropped_frame_id = frame_id;
}
pub fn record_consumed(&mut self, frame_id: u64) {
self.consumed_submissions += 1;
self.last_consumed_frame_id = frame_id;
}
pub fn record_published(&mut self, frame_id: u64) {
self.published_frames += 1;
self.last_published_frame_id = frame_id;
}
pub fn record_presented(&mut self, frame_id: u64) {
self.presented_frames += 1;
self.last_presented_frame_id = frame_id;
}
pub fn record_repeated_frame(&mut self, frame_id: u64) {
self.repeated_frames += 1;
self.last_presented_frame_id = frame_id;
}
pub fn record_stale_submission_discarded(&mut self, frame_id: u64) {
self.stale_submission_discards += 1;
self.last_dropped_frame_id = frame_id;
}
pub fn record_error(&mut self, error: RenderWorkerError, frame_id: u64) {
match error {
RenderWorkerError::BackendUnavailable | RenderWorkerError::RenderFailed => {
self.render_failures += 1;
}
RenderWorkerError::PublishFailed => {
self.publish_failures += 1;
}
RenderWorkerError::PresentFailed => {
self.present_failures += 1;
}
RenderWorkerError::ShutdownTimeout => {
self.shutdown_timeouts += 1;
}
RenderWorkerError::WorkerPanic => {
self.worker_panics += 1;
}
RenderWorkerError::StaleSubmissionDiscarded => {
self.stale_submission_discards += 1;
self.last_dropped_frame_id = frame_id;
}
RenderWorkerError::InternalFailure => {
self.render_failures += 1;
}
}
self.last_error_frame_id = frame_id;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_worker_error_is_copy_equatable_and_debuggable() {
let error = RenderWorkerError::ShutdownTimeout;
let copied = error;
assert_eq!(copied, RenderWorkerError::ShutdownTimeout);
assert_eq!(format!("{:?}", error), "ShutdownTimeout");
assert_eq!(error.to_string(), "render worker shutdown timed out");
}
#[test]
fn render_worker_telemetry_defaults_to_zero() {
assert_eq!(
RenderWorkerTelemetry::default(),
RenderWorkerTelemetry {
produced_submissions: 0,
replaced_before_consume: 0,
consumed_submissions: 0,
published_frames: 0,
presented_frames: 0,
repeated_frames: 0,
stale_submission_discards: 0,
render_failures: 0,
publish_failures: 0,
present_failures: 0,
shutdown_timeouts: 0,
worker_panics: 0,
last_produced_frame_id: 0,
last_consumed_frame_id: 0,
last_published_frame_id: 0,
last_presented_frame_id: 0,
last_dropped_frame_id: 0,
last_error_frame_id: 0,
active_render_epoch: 0,
}
);
}
#[test]
fn render_worker_telemetry_records_counters_and_frame_ids() {
let mut telemetry = RenderWorkerTelemetry::default();
telemetry.record_produced(10);
telemetry.record_replaced_before_consume(10);
telemetry.record_consumed(11);
telemetry.record_published(11);
telemetry.record_presented(11);
telemetry.record_repeated_frame(11);
telemetry.record_error(RenderWorkerError::PublishFailed, 12);
telemetry.record_error(RenderWorkerError::WorkerPanic, 13);
telemetry.record_stale_submission_discarded(14);
assert_eq!(telemetry.produced_submissions, 1);
assert_eq!(telemetry.replaced_before_consume, 1);
assert_eq!(telemetry.consumed_submissions, 1);
assert_eq!(telemetry.published_frames, 1);
assert_eq!(telemetry.presented_frames, 1);
assert_eq!(telemetry.repeated_frames, 1);
assert_eq!(telemetry.publish_failures, 1);
assert_eq!(telemetry.worker_panics, 1);
assert_eq!(telemetry.stale_submission_discards, 1);
assert_eq!(telemetry.last_error_frame_id, 13);
assert_eq!(telemetry.last_dropped_frame_id, 14);
}
}

View File

@ -1,4 +1,5 @@
use crate::log::{LogLevel, LogService, LogSource};
use crate::render_worker::RenderWorkerTelemetry;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering};
@ -34,20 +35,51 @@ pub struct TelemetryFrame {
pub produced_submissions: u64,
pub replaced_before_consume: u64,
pub consumed_submissions: u64,
pub published_frames: u64,
pub presented_frames: u64,
pub repeated_presents: u64,
pub render_errors: u64,
pub publish_errors: u64,
pub present_errors: u64,
pub stale_epoch_discards: u64,
pub shutdown_discards: u64,
pub shutdown_timeouts: u64,
pub worker_panics: u64,
pub last_produced_frame_id: u64,
pub last_consumed_frame_id: u64,
pub last_published_frame_id: u64,
pub last_presented_frame_id: u64,
pub last_dropped_frame_id: u64,
pub last_error_frame_id: u64,
pub active_render_epoch: u64,
}
impl TelemetryFrame {
pub fn render_worker_telemetry(&self) -> RenderWorkerTelemetry {
RenderWorkerTelemetry {
produced_submissions: self.produced_submissions,
replaced_before_consume: self.replaced_before_consume,
consumed_submissions: self.consumed_submissions,
published_frames: self.published_frames,
presented_frames: self.presented_frames,
repeated_frames: self.repeated_presents,
stale_submission_discards: self.stale_epoch_discards,
render_failures: self.render_errors,
publish_failures: self.publish_errors,
present_failures: self.present_errors,
shutdown_timeouts: self.shutdown_timeouts,
worker_panics: self.worker_panics,
last_produced_frame_id: self.last_produced_frame_id,
last_consumed_frame_id: self.last_consumed_frame_id,
last_published_frame_id: self.last_published_frame_id,
last_presented_frame_id: self.last_presented_frame_id,
last_dropped_frame_id: self.last_dropped_frame_id,
last_error_frame_id: self.last_error_frame_id,
active_render_epoch: self.active_render_epoch,
}
}
}
/// Thread-safe, atomic telemetry storage for real-time monitoring by the host.
/// This follows the push-based model from DEC-0005 to avoid expensive scans or locks.
#[derive(Debug, Default)]
@ -84,14 +116,19 @@ pub struct AtomicTelemetry {
pub produced_submissions: AtomicU64,
pub replaced_before_consume: AtomicU64,
pub consumed_submissions: AtomicU64,
pub published_frames: AtomicU64,
pub presented_frames: AtomicU64,
pub repeated_presents: AtomicU64,
pub render_errors: AtomicU64,
pub publish_errors: AtomicU64,
pub present_errors: AtomicU64,
pub stale_epoch_discards: AtomicU64,
pub shutdown_discards: AtomicU64,
pub shutdown_timeouts: AtomicU64,
pub worker_panics: AtomicU64,
pub last_produced_frame_id: AtomicU64,
pub last_consumed_frame_id: AtomicU64,
pub last_published_frame_id: AtomicU64,
pub last_presented_frame_id: AtomicU64,
pub last_dropped_frame_id: AtomicU64,
pub last_error_frame_id: AtomicU64,
@ -128,14 +165,19 @@ impl AtomicTelemetry {
produced_submissions: self.produced_submissions.load(Ordering::Relaxed),
replaced_before_consume: self.replaced_before_consume.load(Ordering::Relaxed),
consumed_submissions: self.consumed_submissions.load(Ordering::Relaxed),
published_frames: self.published_frames.load(Ordering::Relaxed),
presented_frames: self.presented_frames.load(Ordering::Relaxed),
repeated_presents: self.repeated_presents.load(Ordering::Relaxed),
render_errors: self.render_errors.load(Ordering::Relaxed),
publish_errors: self.publish_errors.load(Ordering::Relaxed),
present_errors: self.present_errors.load(Ordering::Relaxed),
stale_epoch_discards: self.stale_epoch_discards.load(Ordering::Relaxed),
shutdown_discards: self.shutdown_discards.load(Ordering::Relaxed),
shutdown_timeouts: self.shutdown_timeouts.load(Ordering::Relaxed),
worker_panics: self.worker_panics.load(Ordering::Relaxed),
last_produced_frame_id: self.last_produced_frame_id.load(Ordering::Relaxed),
last_consumed_frame_id: self.last_consumed_frame_id.load(Ordering::Relaxed),
last_published_frame_id: self.last_published_frame_id.load(Ordering::Relaxed),
last_presented_frame_id: self.last_presented_frame_id.load(Ordering::Relaxed),
last_dropped_frame_id: self.last_dropped_frame_id.load(Ordering::Relaxed),
last_error_frame_id: self.last_error_frame_id.load(Ordering::Relaxed),
@ -165,14 +207,19 @@ impl AtomicTelemetry {
self.produced_submissions.store(0, Ordering::Relaxed);
self.replaced_before_consume.store(0, Ordering::Relaxed);
self.consumed_submissions.store(0, Ordering::Relaxed);
self.published_frames.store(0, Ordering::Relaxed);
self.presented_frames.store(0, Ordering::Relaxed);
self.repeated_presents.store(0, Ordering::Relaxed);
self.render_errors.store(0, Ordering::Relaxed);
self.publish_errors.store(0, Ordering::Relaxed);
self.present_errors.store(0, Ordering::Relaxed);
self.stale_epoch_discards.store(0, Ordering::Relaxed);
self.shutdown_discards.store(0, Ordering::Relaxed);
self.shutdown_timeouts.store(0, Ordering::Relaxed);
self.worker_panics.store(0, Ordering::Relaxed);
self.last_produced_frame_id.store(0, Ordering::Relaxed);
self.last_consumed_frame_id.store(0, Ordering::Relaxed);
self.last_published_frame_id.store(0, Ordering::Relaxed);
self.last_presented_frame_id.store(0, Ordering::Relaxed);
self.last_dropped_frame_id.store(0, Ordering::Relaxed);
self.last_error_frame_id.store(0, Ordering::Relaxed);
@ -398,4 +445,51 @@ mod tests {
assert_eq!(snapshot.vm_heap_allocations, 2);
assert_eq!(snapshot.vm_string_materializations, 5);
}
#[test]
fn snapshot_maps_render_worker_telemetry() {
let current = Arc::new(AtomicU32::new(0));
let tel = AtomicTelemetry::new(current);
tel.produced_submissions.store(2, Ordering::Relaxed);
tel.replaced_before_consume.store(1, Ordering::Relaxed);
tel.consumed_submissions.store(1, Ordering::Relaxed);
tel.published_frames.store(1, Ordering::Relaxed);
tel.presented_frames.store(1, Ordering::Relaxed);
tel.repeated_presents.store(3, Ordering::Relaxed);
tel.render_errors.store(4, Ordering::Relaxed);
tel.publish_errors.store(5, Ordering::Relaxed);
tel.present_errors.store(6, Ordering::Relaxed);
tel.stale_epoch_discards.store(7, Ordering::Relaxed);
tel.shutdown_timeouts.store(8, Ordering::Relaxed);
tel.worker_panics.store(9, Ordering::Relaxed);
tel.last_produced_frame_id.store(10, Ordering::Relaxed);
tel.last_consumed_frame_id.store(11, Ordering::Relaxed);
tel.last_published_frame_id.store(12, Ordering::Relaxed);
tel.last_presented_frame_id.store(13, Ordering::Relaxed);
tel.last_dropped_frame_id.store(14, Ordering::Relaxed);
tel.last_error_frame_id.store(15, Ordering::Relaxed);
tel.active_render_epoch.store(16, Ordering::Relaxed);
let worker = tel.snapshot().render_worker_telemetry();
assert_eq!(worker.produced_submissions, 2);
assert_eq!(worker.replaced_before_consume, 1);
assert_eq!(worker.consumed_submissions, 1);
assert_eq!(worker.published_frames, 1);
assert_eq!(worker.presented_frames, 1);
assert_eq!(worker.repeated_frames, 3);
assert_eq!(worker.render_failures, 4);
assert_eq!(worker.publish_failures, 5);
assert_eq!(worker.present_failures, 6);
assert_eq!(worker.stale_submission_discards, 7);
assert_eq!(worker.shutdown_timeouts, 8);
assert_eq!(worker.worker_panics, 9);
assert_eq!(worker.last_produced_frame_id, 10);
assert_eq!(worker.last_consumed_frame_id, 11);
assert_eq!(worker.last_published_frame_id, 12);
assert_eq!(worker.last_presented_frame_id, 13);
assert_eq!(worker.last_dropped_frame_id, 14);
assert_eq!(worker.last_error_frame_id, 15);
assert_eq!(worker.active_render_epoch, 16);
}
}

View File

@ -6,8 +6,12 @@ mod services;
pub use crash_report::CrashReport;
pub use os::{LifecycleError, LifecycleOperation, SystemOS};
pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink};
pub use services::fs;
pub use services::process;
pub use services::task;
pub use services::vm_runtime::VirtualMachineRuntime;
pub use services::vm_runtime::{
RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff, RenderWorkerHandoffWait,
RenderWorkerOwnership, VirtualMachineRuntime,
};
pub use services::windows;

View File

@ -1,9 +1,13 @@
use crate::CrashReport;
use crate::os::SystemOS;
use crate::{RenderWorkerConfig, RenderWorkerController};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
use prometeu_hal::{InputSignals, RuntimePlatform};
use prometeu_hal::{
InputSignals, OwnedRgba8888Frame, RenderWorkerBackend, RenderWorkerError,
RenderWorkerFrameSink, RuntimePlatform,
};
use prometeu_vm::VirtualMachine;
use std::sync::atomic::Ordering;
@ -96,6 +100,30 @@ impl<'a> VmFacade<'a> {
self.os.vm_runtime.atomic_telemetry.snapshot()
}
pub fn start_render_worker<B, S>(&mut self, config: RenderWorkerConfig, backend: B, sink: S)
where
B: RenderWorkerBackend,
S: RenderWorkerFrameSink,
{
self.os.vm_runtime.start_render_worker(config, backend, sink);
}
pub fn stop_render_worker(&mut self) -> Result<(), RenderWorkerError> {
self.os.vm_runtime.stop_render_worker()
}
pub fn render_worker_controller(&self) -> Option<&RenderWorkerController> {
self.os.vm_runtime.render_worker_controller.as_ref()
}
pub fn latest_render_worker_frame(&self) -> Option<OwnedRgba8888Frame> {
self.os.vm_runtime.latest_render_worker_frame()
}
pub fn repeat_latest_render_worker_frame(&self) -> Option<OwnedRgba8888Frame> {
self.os.vm_runtime.repeat_latest_render_worker_frame()
}
pub fn cert_config(&self) -> &CertificationConfig {
&self.os.vm_runtime.certifier.config
}

View File

@ -112,7 +112,7 @@ fn matrix_packet_policy_and_pacing_are_mode_specific() {
assert_eq!(game_policy.execution, RenderExecutionPolicy::WorkerCapableWithLocalFallback);
assert_eq!(
game_policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }),
RenderConsumerPath::LocalWorkerPrototype
RenderConsumerPath::RealWorker
);
let shell_policy = RenderPolicy::for_app_mode(AppMode::Shell);
@ -163,7 +163,7 @@ fn matrix_sync_and_worker_paths_share_latest_wins_handoff_contract() {
let outcome = match path {
RenderConsumerPath::LocalSynchronous => manager.consume_latest(&mut surface),
RenderConsumerPath::LocalWorkerPrototype => {
RenderConsumerPath::RealWorker => {
let mut worker = LocalRenderWorker;
worker.consume_latest(&mut manager, &mut surface)
}
@ -172,7 +172,7 @@ fn matrix_sync_and_worker_paths_share_latest_wins_handoff_contract() {
(outcome, surface.seen, manager.render_telemetry().replaced_before_consume)
}
for path in [RenderConsumerPath::LocalSynchronous, RenderConsumerPath::LocalWorkerPrototype] {
for path in [RenderConsumerPath::LocalSynchronous, RenderConsumerPath::RealWorker] {
let (outcome, seen, replaced) = consume_with(path);
assert_eq!(outcome, RenderConsumeOutcome::Presented { frame_id: FrameId::new(1) });
assert_eq!(seen, vec![FrameId::new(1)]);

View File

@ -3,6 +3,9 @@ use crate::CrashReport;
use crate::fs::{FsBackend, FsState, VirtualFS};
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::{
OwnedRgba8888Frame, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink,
};
use std::collections::HashMap;
impl VirtualMachineRuntime {
@ -25,6 +28,9 @@ impl VirtualMachineRuntime {
frame_scheduler: FrameScheduler::default(),
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(),
logs_written_this_frame: HashMap::new(),
@ -124,6 +130,10 @@ impl VirtualMachineRuntime {
self.frame_scheduler = FrameScheduler::default();
self.render_capabilities = RenderRuntimeCapabilities::default();
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();
@ -147,6 +157,52 @@ impl VirtualMachineRuntime {
self.render_capabilities = capabilities;
}
pub fn start_render_worker<B, S>(&mut self, config: RenderWorkerConfig, backend: B, sink: S)
where
B: RenderWorkerBackend,
S: RenderWorkerFrameSink,
{
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,
));
self.render_capabilities.game_render_worker = true;
}
pub fn stop_render_worker(&mut self) -> Result<(), RenderWorkerError> {
if let Some(mut controller) = self.render_worker_controller.take() {
controller.stop()
} else {
Ok(())
}
}
pub(crate) fn publish_pending_render_to_worker(&mut self) -> bool {
let Some(submission) = self.render_manager.take_pending_for_worker() else {
return false;
};
self.render_worker_handoff.publish(submission);
true
}
pub(crate) fn sync_render_worker_ownership(&self) {
self.render_worker_ownership.set(self.render_manager.active_ownership());
}
pub fn latest_render_worker_frame(&self) -> Option<OwnedRgba8888Frame> {
self.render_worker_controller.as_ref()?.latest_published_frame()
}
pub fn repeat_latest_render_worker_frame(&self) -> Option<OwnedRgba8888Frame> {
self.render_worker_controller.as_ref()?.repeat_latest_frame()
}
pub fn initialize_vm(
&mut self,
log_service: &mut LogService,

View File

@ -4,6 +4,10 @@ mod dispatch;
mod frame_scheduler;
mod lifecycle;
pub mod render_manager;
mod render_worker;
pub mod render_worker_handoff;
#[cfg(test)]
pub(crate) mod render_worker_test_harness;
#[cfg(test)]
mod tests;
mod tick;
@ -17,6 +21,8 @@ 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, RenderWorkerOwnership};
pub use render_worker_handoff::{RenderWorkerHandoff, RenderWorkerHandoffWait};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicU32;
@ -35,6 +41,9 @@ pub struct VirtualMachineRuntime {
pub frame_scheduler: FrameScheduler,
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>,
pub logs_written_this_frame: HashMap<u32, u32>,

View File

@ -59,7 +59,7 @@ pub struct RenderRuntimeCapabilities {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderConsumerPath {
LocalSynchronous,
LocalWorkerPrototype,
RealWorker,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -95,7 +95,7 @@ impl RenderPolicy {
) -> RenderConsumerPath {
match (self.execution, capabilities.game_render_worker) {
(RenderExecutionPolicy::WorkerCapableWithLocalFallback, true) => {
RenderConsumerPath::LocalWorkerPrototype
RenderConsumerPath::RealWorker
}
_ => RenderConsumerPath::LocalSynchronous,
}
@ -107,8 +107,10 @@ pub trait RenderSurface {
}
#[derive(Debug, Default)]
#[cfg(test)]
pub struct LocalRenderWorker;
#[cfg(test)]
impl LocalRenderWorker {
pub fn consume_latest<S: RenderSurface>(
&mut self,
@ -329,6 +331,12 @@ impl RenderManager {
discarded
}
pub fn take_pending_for_worker(&mut self) -> Option<RenderSubmission> {
let submission = self.handoff.take_latest()?;
self.telemetry.record_consumed(submission.frame_id);
Some(submission)
}
pub fn request_shutdown(&mut self) -> Option<RenderSubmission> {
self.consumer_state = RenderConsumerState::Stopped;
self.discard_pending_submission()
@ -624,7 +632,7 @@ mod tests {
);
assert_eq!(
policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }),
RenderConsumerPath::LocalWorkerPrototype
RenderConsumerPath::RealWorker
);
}

View File

@ -0,0 +1,456 @@
use crate::services::vm_runtime::{RenderWorkerHandoff, RenderWorkerHandoffWait};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::{
FrameId, OwnedRgba8888Frame, RenderOwnership, RenderWorkerBackend, RenderWorkerError,
RenderWorkerFrameSink, RenderWorkerTelemetry,
};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RenderWorkerConfig {
pub shutdown_timeout: Duration,
}
impl Default for RenderWorkerConfig {
fn default() -> Self {
Self { shutdown_timeout: Duration::from_millis(250) }
}
}
#[derive(Debug, Default)]
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>>,
}
impl RenderWorkerController {
pub fn start<B, S>(
config: RenderWorkerConfig,
handoff: Arc<RenderWorkerHandoff>,
ownership: RenderWorkerOwnership,
backend: B,
sink: S,
) -> Self
where
B: RenderWorkerBackend,
S: RenderWorkerFrameSink,
{
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_ownership, worker_state, backend, sink);
}))
.map_err(|_| RenderWorkerError::WorkerPanic);
if let Err(error) = result {
record_error(&panic_state, error, FrameId::ZERO);
}
let _ = done_tx.send(result);
});
Self { config, handoff, ownership, state, handle: Some(handle), done_rx }
}
pub fn stop(&mut self) -> Result<(), RenderWorkerError> {
self.handoff.request_shutdown();
match self.done_rx.recv_timeout(self.config.shutdown_timeout) {
Ok(result) => {
if let Some(handle) = self.handle.take() {
if handle.join().is_err() {
record_error(&self.state, RenderWorkerError::WorkerPanic, FrameId::ZERO);
return Err(RenderWorkerError::WorkerPanic);
}
}
result
}
Err(RecvTimeoutError::Timeout) => {
record_error(&self.state, RenderWorkerError::ShutdownTimeout, FrameId::ZERO);
Err(RenderWorkerError::ShutdownTimeout)
}
Err(RecvTimeoutError::Disconnected) => {
record_error(&self.state, RenderWorkerError::InternalFailure, FrameId::ZERO);
Err(RenderWorkerError::InternalFailure)
}
}
}
pub fn telemetry(&self) -> RenderWorkerTelemetry {
let mut telemetry = self.handoff.telemetry();
let state = self.state.lock().unwrap();
telemetry.published_frames = state.telemetry.published_frames;
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
}
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 {
fn drop(&mut self) {
if self.handle.is_some() {
let _ = self.stop();
}
}
}
fn run_worker_loop<B, S>(
handoff: Arc<RenderWorkerHandoff>,
ownership: RenderWorkerOwnership,
state: Arc<Mutex<RenderWorkerState>>,
backend: B,
sink: S,
) where
B: RenderWorkerBackend,
S: RenderWorkerFrameSink,
{
loop {
let submission = match handoff.wait_take() {
RenderWorkerHandoffWait::Submission(submission) => submission,
RenderWorkerHandoffWait::Shutdown => return,
};
let frame_id = submission.frame_id;
let render_result = catch_unwind(AssertUnwindSafe(|| backend.render(&submission)))
.map_err(|_| RenderWorkerError::WorkerPanic);
let frame = match render_result {
Ok(Ok(frame)) => frame,
Ok(Err(error)) | Err(error) => {
record_error(&state, error, frame_id);
continue;
}
};
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),
Ok(Err(error)) | Err(error) => record_error(&state, error, frame_id),
}
}
}
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(
state: &Arc<Mutex<RenderWorkerState>>,
error: RenderWorkerError,
frame_id: FrameId,
) {
let mut state = state.lock().unwrap();
state.last_error = Some(error);
state.telemetry.record_error(error, frame_id.get());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::services::vm_runtime::VirtualMachineRuntime;
use crate::services::vm_runtime::render_worker_test_harness::{
FakeRenderBackend, RenderGate, game_submission,
};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::{
FrameId, Game2DFramePacket, OwnedRgba8888Frame, RenderOwnership, RenderSubmission,
};
#[derive(Clone, Debug)]
struct SharedFakeBackend(Arc<FakeRenderBackend>);
impl RenderWorkerBackend for SharedFakeBackend {
fn render(
&self,
submission: &RenderSubmission,
) -> Result<OwnedRgba8888Frame, RenderWorkerError> {
self.0.render(submission)
}
}
impl RenderWorkerFrameSink for SharedFakeBackend {
fn publish(&self, frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError> {
self.0.publish(frame);
Ok(())
}
}
#[derive(Debug, Default)]
struct PanickingBackend;
impl RenderWorkerBackend for PanickingBackend {
fn render(
&self,
_submission: &RenderSubmission,
) -> Result<OwnedRgba8888Frame, RenderWorkerError> {
panic!("backend panic")
}
}
#[derive(Debug, Default)]
struct NoopSink;
impl RenderWorkerFrameSink for NoopSink {
fn publish(&self, _frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError> {
Ok(())
}
}
#[test]
fn render_worker_controller_starts_and_stops_waiting_worker() {
let handoff = Arc::new(RenderWorkerHandoff::default());
let backend = Arc::new(FakeRenderBackend::default());
let mut controller = RenderWorkerController::start(
RenderWorkerConfig::default(),
Arc::clone(&handoff),
RenderWorkerOwnership::default(),
SharedFakeBackend(Arc::clone(&backend)),
SharedFakeBackend(backend),
);
assert_eq!(controller.stop(), Ok(()));
assert_eq!(controller.last_error(), None);
}
#[test]
fn render_worker_controller_renders_and_publishes_submission() {
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(11, 1, 1));
assert_eq!(controller.stop(), Ok(()));
let frames = backend.published_frames();
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].frame_id, FrameId::new(11));
assert_eq!(controller.telemetry().published_frames, 1);
}
#[test]
fn render_worker_controller_reports_shutdown_timeout() {
let handoff = Arc::new(RenderWorkerHandoff::default());
let gate = RenderGate::default();
let backend = Arc::new(FakeRenderBackend::with_gate(gate.clone()));
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)),
);
handoff.publish(game_submission(12, 1, 1));
gate.wait_for_entries(1);
assert_eq!(controller.stop(), Err(RenderWorkerError::ShutdownTimeout));
assert_eq!(controller.last_error(), Some(RenderWorkerError::ShutdownTimeout));
assert_eq!(controller.telemetry().shutdown_timeouts, 1);
gate.release_one();
controller.config.shutdown_timeout = Duration::from_millis(250);
assert_eq!(controller.stop(), Ok(()));
}
#[test]
fn render_worker_controller_captures_backend_panic_as_typed_failure() {
let handoff = Arc::new(RenderWorkerHandoff::default());
let mut controller = RenderWorkerController::start(
RenderWorkerConfig::default(),
Arc::clone(&handoff),
RenderWorkerOwnership::default(),
PanickingBackend,
NoopSink,
);
handoff.publish(game_submission(13, 1, 1));
assert_eq!(controller.stop(), Ok(()));
assert_eq!(controller.last_error(), Some(RenderWorkerError::WorkerPanic));
assert_eq!(controller.telemetry().worker_panics, 1);
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();
let backend = Arc::new(FakeRenderBackend::with_gate(gate.clone()));
let mut runtime = VirtualMachineRuntime::new(None);
runtime.start_render_worker(
RenderWorkerConfig::default(),
SharedFakeBackend(Arc::clone(&backend)),
SharedFakeBackend(Arc::clone(&backend)),
);
runtime
.render_manager
.close_frame_with_packet(prometeu_hal::RenderSubmissionPacket::Game2D(
Game2DFramePacket::default(),
))
.expect("game packet should close");
assert!(runtime.publish_pending_render_to_worker());
gate.wait_for_entries(1);
runtime
.render_manager
.close_frame_with_packet(prometeu_hal::RenderSubmissionPacket::Game2D(
Game2DFramePacket::default(),
))
.expect("second game packet should close");
assert!(runtime.publish_pending_render_to_worker());
runtime
.render_manager
.close_frame_with_packet(prometeu_hal::RenderSubmissionPacket::Game2D(
Game2DFramePacket::default(),
))
.expect("third game packet should close");
assert!(runtime.publish_pending_render_to_worker());
assert_eq!(runtime.render_worker_handoff.telemetry().replaced_before_consume, 1);
gate.release_one();
gate.wait_for_entries(2);
gate.release_one();
assert_eq!(runtime.stop_render_worker(), Ok(()));
let frames = backend.published_frames();
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].frame_id, FrameId::ZERO);
assert_eq!(frames[1].frame_id, FrameId::new(2));
}
}

View File

@ -0,0 +1,172 @@
use prometeu_hal::{RenderSubmission, RenderWorkerTelemetry};
use std::sync::{Condvar, Mutex};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderWorkerHandoffWait {
Submission(RenderSubmission),
Shutdown,
}
#[derive(Debug, Default)]
struct RenderWorkerHandoffState {
pending: Option<RenderSubmission>,
shutdown_requested: bool,
telemetry: RenderWorkerTelemetry,
}
#[derive(Debug, Default)]
pub struct RenderWorkerHandoff {
state: Mutex<RenderWorkerHandoffState>,
ready: Condvar,
}
impl RenderWorkerHandoff {
pub fn publish(&self, submission: RenderSubmission) {
let mut state = self.state.lock().unwrap();
if let Some(replaced) = state.pending.replace(submission) {
state.telemetry.record_replaced_before_consume(replaced.frame_id.get());
}
let frame_id = state.pending.as_ref().expect("submission was just stored").frame_id;
state.telemetry.record_produced(frame_id.get());
self.ready.notify_one();
}
pub fn take_latest(&self) -> Option<RenderSubmission> {
let mut state = self.state.lock().unwrap();
let submission = state.pending.take()?;
state.telemetry.record_consumed(submission.frame_id.get());
Some(submission)
}
pub fn wait_take(&self) -> RenderWorkerHandoffWait {
self.wait_take_with_hook(|| {})
}
pub fn request_shutdown(&self) {
let mut state = self.state.lock().unwrap();
state.shutdown_requested = true;
self.ready.notify_all();
}
pub fn telemetry(&self) -> RenderWorkerTelemetry {
self.state.lock().unwrap().telemetry
}
fn wait_take_with_hook(&self, mut before_wait: impl FnMut()) -> RenderWorkerHandoffWait {
let mut state = self.state.lock().unwrap();
loop {
if let Some(submission) = state.pending.take() {
state.telemetry.record_consumed(submission.frame_id.get());
return RenderWorkerHandoffWait::Submission(submission);
}
if state.shutdown_requested {
return RenderWorkerHandoffWait::Shutdown;
}
before_wait();
state = self.ready.wait(state).unwrap();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use prometeu_hal::{FrameId, Game2DFramePacket};
use std::sync::Arc;
use std::sync::mpsc;
use std::thread;
fn submission(frame_id: u64) -> RenderSubmission {
RenderSubmission::game2d(FrameId::new(frame_id), Game2DFramePacket::default())
}
#[test]
fn render_worker_handoff_takes_owned_submission() {
let handoff = RenderWorkerHandoff::default();
assert!(handoff.take_latest().is_none());
handoff.publish(submission(7));
let taken = handoff.take_latest().expect("pending submission");
assert_eq!(taken.frame_id, FrameId::new(7));
assert!(handoff.take_latest().is_none());
assert_eq!(handoff.telemetry().produced_submissions, 1);
assert_eq!(handoff.telemetry().consumed_submissions, 1);
}
#[test]
fn render_worker_handoff_wait_take_returns_pending_without_blocking() {
let handoff = RenderWorkerHandoff::default();
handoff.publish(submission(8));
assert_eq!(handoff.wait_take(), RenderWorkerHandoffWait::Submission(submission(8)));
assert!(handoff.take_latest().is_none());
}
#[test]
fn render_worker_handoff_is_latest_wins_and_counts_replacements() {
let handoff = RenderWorkerHandoff::default();
handoff.publish(submission(1));
handoff.publish(submission(2));
handoff.publish(submission(3));
let taken = handoff.take_latest().expect("latest submission");
let telemetry = handoff.telemetry();
assert_eq!(taken.frame_id, FrameId::new(3));
assert_eq!(telemetry.produced_submissions, 3);
assert_eq!(telemetry.replaced_before_consume, 2);
assert_eq!(telemetry.consumed_submissions, 1);
assert_eq!(telemetry.last_produced_frame_id, 3);
assert_eq!(telemetry.last_dropped_frame_id, 2);
}
#[test]
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();
let consumer = thread::spawn(move || {
let mut waiting_tx = Some(waiting_tx);
consumer_handoff.wait_take_with_hook(|| {
if let Some(tx) = waiting_tx.take() {
tx.send(()).unwrap();
}
})
});
waiting_rx.recv().expect("consumer reached condvar wait");
handoff.publish(submission(42));
assert_eq!(
consumer.join().expect("consumer thread should finish"),
RenderWorkerHandoffWait::Submission(submission(42))
);
assert_eq!(handoff.telemetry().consumed_submissions, 1);
}
#[test]
fn render_worker_handoff_wait_returns_shutdown_signal() {
let handoff = Arc::new(RenderWorkerHandoff::default());
let consumer_handoff = Arc::clone(&handoff);
let (waiting_tx, waiting_rx) = mpsc::channel();
let consumer = thread::spawn(move || {
let mut waiting_tx = Some(waiting_tx);
consumer_handoff.wait_take_with_hook(|| {
if let Some(tx) = waiting_tx.take() {
tx.send(()).unwrap();
}
})
});
waiting_rx.recv().expect("consumer reached condvar wait");
handoff.request_shutdown();
assert_eq!(
consumer.join().expect("consumer thread should finish"),
RenderWorkerHandoffWait::Shutdown
);
}
}

View File

@ -0,0 +1,173 @@
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);
}
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) {
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");
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();

View File

@ -1,7 +1,5 @@
use super::dispatch::VmRuntimeHost;
use super::render_manager::{
LocalRenderWorker, RenderConsumeOutcome, RenderConsumerPath, RenderSurface,
};
use super::render_manager::{RenderConsumeOutcome, RenderConsumerPath, RenderSurface};
use super::*;
use crate::CrashReport;
use crate::fs::{FsState, VirtualFS};
@ -248,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);
@ -273,9 +272,18 @@ impl VirtualMachineRuntime {
RenderConsumerPath::LocalSynchronous => {
self.render_manager.consume_latest(&mut surface)
}
RenderConsumerPath::LocalWorkerPrototype => {
let mut worker = LocalRenderWorker;
worker.consume_latest(&mut self.render_manager, &mut surface)
RenderConsumerPath::RealWorker
if self.render_worker_controller.is_some()
&& self.current_cartridge_app_mode == AppMode::Game =>
{
if self.publish_pending_render_to_worker() {
RenderConsumeOutcome::NoSubmission
} else {
RenderConsumeOutcome::NoSubmission
}
}
RenderConsumerPath::RealWorker => {
self.render_manager.consume_latest(&mut surface)
}
}
}));

View File

@ -398,17 +398,13 @@ mod tests {
let mut firmware = Firmware::new(None);
let mut platform = TestPlatform::new();
debugger.handle_command(DebugCommand::Pause, &mut firmware, platform.local_hardware_mut());
debugger.handle_command(DebugCommand::Pause, &mut firmware, &mut platform);
assert!(firmware.os.vm().paused());
debugger.handle_command(DebugCommand::Resume, &mut firmware, platform.local_hardware_mut());
debugger.handle_command(DebugCommand::Resume, &mut firmware, &mut platform);
assert!(!firmware.os.vm().paused());
debugger.handle_command(
DebugCommand::StepFrame,
&mut firmware,
platform.local_hardware_mut(),
);
debugger.handle_command(DebugCommand::StepFrame, &mut firmware, &mut platform);
assert!(!firmware.os.vm().paused());
assert!(firmware.os.vm().debug_step_requested());
}
@ -422,7 +418,7 @@ mod tests {
debugger.waiting_for_start = true;
firmware.os.vm().set_paused(true);
debugger.handle_command(DebugCommand::Start, &mut firmware, platform.local_hardware_mut());
debugger.handle_command(DebugCommand::Start, &mut firmware, &mut platform);
assert!(!debugger.waiting_for_start);
assert!(!firmware.os.vm().paused());

View File

@ -4,14 +4,16 @@ use crate::fs_backend::HostDirBackend;
use crate::input::HostInputHandler;
use crate::log_sink::HostConsoleSink;
use crate::stats::HostStats;
use crate::utilities::draw_rgba8888_to_rgba8;
use crate::utilities::{draw_owned_rgba8888_frame_to_rgba8, draw_rgba8888_to_rgba8};
use pixels::wgpu::PresentMode;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use prometeu_drivers::AudioCommand;
use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks};
use prometeu_firmware::{BootTarget, Firmware, FirmwareState};
use prometeu_hal::RuntimePlatform;
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{OwnedRgba8888Frame, RenderWorkerError, RenderWorkerFrameSink, RuntimePlatform};
use prometeu_system::RenderWorkerConfig;
use std::sync::Arc;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
@ -22,6 +24,15 @@ use winit::window::{Window, WindowAttributes, WindowId};
const IDLE_HOST_POLL_DT: Duration = Duration::from_millis(100);
#[derive(Debug, Clone, Copy)]
struct HostWorkerFrameSink;
impl RenderWorkerFrameSink for HostWorkerFrameSink {
fn publish(&self, _frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError> {
Ok(())
}
}
#[derive(Debug, Clone)]
struct PresentationState {
latest_published_frame: u64,
@ -86,6 +97,10 @@ fn desired_control_flow(
}
}
fn should_present_worker_frame(state: &FirmwareState) -> bool {
matches!(state, FirmwareState::GameRunning(_))
}
/// The Desktop implementation of the PROMETEU Runtime.
///
/// This struct acts as the physical "chassis" of the virtual console. It is
@ -153,8 +168,20 @@ impl HostRunner {
firmware.os.fs().mount(Box::new(backend));
}
let hardware = Hardware::new();
let memory_banks = Arc::new(MemoryBanks::new());
let hardware = Hardware::new_with_memory_banks(Arc::clone(&memory_banks));
let display_size = hardware.display_size();
let worker_backend = LocalFramebufferRenderBackend::new_with_memory_banks(
display_size.0,
display_size.1,
memory_banks,
hardware.assets.glyph_asset_slot_index(),
);
firmware.os.vm().start_render_worker(
RenderWorkerConfig::default(),
worker_backend,
HostWorkerFrameSink,
);
Self {
window: None,
@ -278,10 +305,15 @@ impl ApplicationHandler for HostRunner {
// Mutable borrow of the frame (lasts only within this block)
let frame = pixels.frame_mut();
// Immutable borrow of prometeu-core (different field, ok)
let src = self.hardware.gfx.front_buffer();
draw_rgba8888_to_rgba8(src, frame);
if should_present_worker_frame(&self.firmware.state)
&& let Some(worker_frame) =
self.firmware.os.vm().repeat_latest_render_worker_frame()
{
draw_owned_rgba8888_frame_to_rgba8(&worker_frame, frame);
} else {
let src = self.hardware.gfx.front_buffer();
draw_rgba8888_to_rgba8(src, frame);
}
} // <- frame borrow ends here
if pixels.render().is_err() {
@ -372,7 +404,13 @@ impl ApplicationHandler for HostRunner {
self.stats.record_frame();
}
self.presentation.note_published_frame(self.firmware.os.frame_index());
if should_present_worker_frame(&self.firmware.state)
&& let Some(worker_frame) = self.firmware.os.vm().latest_render_worker_frame()
{
self.presentation.note_published_frame(worker_frame.frame_id.get());
} else {
self.presentation.note_published_frame(self.firmware.os.frame_index());
}
if was_debugger_connected != self.debugger.stream.is_some()
|| was_waiting_for_start != self.debugger.waiting_for_start
@ -412,8 +450,12 @@ impl ApplicationHandler for HostRunner {
mod tests {
use super::*;
use prometeu_firmware::BootTarget;
use prometeu_firmware::firmware::firmware_state::{
GameRunningStep, HubHomeStep, ShellRunningStep,
};
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
use prometeu_system::task::TaskId;
use std::io::{Read, Write};
use std::net::TcpStream;
@ -481,6 +523,17 @@ mod tests {
}
}
#[test]
fn worker_frame_presentation_is_game_only() {
assert!(should_present_worker_frame(&FirmwareState::GameRunning(GameRunningStep::new(
TaskId(1),
))));
assert!(!should_present_worker_frame(&FirmwareState::ShellRunning(ShellRunningStep::new(
TaskId(2)
),)));
assert!(!should_present_worker_frame(&FirmwareState::HubHome(HubHomeStep)));
}
#[test]
fn host_debugger_maps_cert_events_from_host_owned_sources() {
let telemetry = TelemetryFrame {

View File

@ -1,3 +1,5 @@
use prometeu_hal::OwnedRgba8888Frame;
/// Copies RGBA8888 pixels in canonical RGBA raw order into the `pixels` RGBA8 frame.
pub fn draw_rgba8888_to_rgba8(src: &[u32], dst_rgba: &mut [u8]) {
for (i, &px) in src.iter().enumerate() {
@ -9,9 +11,15 @@ pub fn draw_rgba8888_to_rgba8(src: &[u32], dst_rgba: &mut [u8]) {
}
}
pub fn draw_owned_rgba8888_frame_to_rgba8(frame: &OwnedRgba8888Frame, dst_rgba: &mut [u8]) {
draw_rgba8888_to_rgba8(&frame.pixels[..frame.required_pixel_len()], dst_rgba);
}
#[cfg(test)]
mod tests {
use super::draw_rgba8888_to_rgba8;
use super::{draw_owned_rgba8888_frame_to_rgba8, draw_rgba8888_to_rgba8};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::{FrameId, OwnedRgba8888Frame, RenderOwnership};
#[test]
fn draw_rgba8888_to_rgba8_preserves_channel_order_and_alpha() {
@ -22,4 +30,21 @@ mod tests {
assert_eq!(dst, [0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x01]);
}
#[test]
fn draw_owned_frame_to_rgba8_uses_owned_rgba8888_pixels() {
let frame = OwnedRgba8888Frame::packed(
FrameId::new(3),
RenderOwnership::new(1, AppMode::Game, 7),
1,
1,
vec![0x10203040],
)
.expect("test frame should be valid");
let mut dst = [0; 4];
draw_owned_rgba8888_frame_to_rgba8(&frame, &mut dst);
assert_eq!(dst, [0x10, 0x20, 0x30, 0x40]);
}
}

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
id: AGD-0043
ticket: real-render-worker-establishment
title: Real Render Worker Establishment
status: open
status: accepted
created: 2026-06-06
resolved:
decision:
@ -11,9 +11,24 @@ tags: [runtime, renderer, worker, concurrency, host, hal, architecture]
## Contexto
`DEC-0031` definiu o contrato arquitetural da fronteira VM/render. `AGD-0042` prepara a fronteira host/hardware/render para que `Hardware` deixe de ser um objeto concreto atravessando o worker e passe a ser contrato/facades implementadas pelo host.
`DEC-0031` definiu o contrato arquitetural da fronteira VM/render. `AGD-0042` preparou a fronteira host/hardware/render para que `Hardware` deixe de ser um objeto concreto atravessando o worker e passe a ser contrato/facades implementadas pelo host.
Esta agenda discute a implementacao do worker real propriamente dito, assumindo que a preparacao da fronteira foi ou sera resolvida antes da execucao.
Esta agenda discute a implementacao do worker real propriamente dito, assumindo que a preparacao da fronteira ja foi resolvida antes da execucao.
### Estado Atual Depois da `AGD-0042`
A preparacao de fronteira foi executada pelos planos `PLN-0098` a `PLN-0109`:
- `HardwareBridge` foi removido do codigo e deixou de ser contrato publico;
- `RuntimePlatform` passou a ser a fronteira runtime-facing para render, input, audio, assets e telemetry;
- `RenderSubmissionSink` aceita submissao owned;
- `Game2DFrameComposer` separa fechamento logico de frame do backend de render;
- syscalls `gfx2d.*` e `gfxui.*` bufferizam comandos e nao desenham imediatamente via `Gfx`;
- firmware, Hub, runtime e host desktop passam por servicos de plataforma;
- testes de runtime/firmware usam `TestPlatform` como fixture local;
- specs de GFX/portabilidade documentam que o worker/render consumer nao deve depender de `&mut Hardware`, `&mut Gfx`, `FrameComposer` vivo ou estado mutavel da VM.
O estado atual ainda nao tem worker real. O `LocalRenderWorker` continua sendo o caminho local/cooperativo que consome `RenderSubmission` no mesmo processo. O proximo passo desta agenda e decidir onde vive o controller do worker real, qual handoff thread-safe ele usa, e qual backend renderizavel ele owns ou recebe.
## Problema
@ -124,14 +139,69 @@ O host desktop pode ser a primeira integracao concreta, mas nao deve ser o unico
## Perguntas em Aberto
- [ ] O worker controller vive em `prometeu-system`, `prometeu-hal`, `prometeu-drivers`, ou host?
- [ ] O handoff thread-safe sera `Mutex<Option<...>>`, canal bounded, atomic slot, ou estrutura propria?
- [ ] Como modelar current work para shutdown e stale owner?
- [ ] Quem chama present: worker, host event loop, ou display cadence service?
- [ ] Como o worker recebe recursos read-only preparados na `AGD-0042`?
- [ ] Qual erro tipado minimo precisamos no primeiro worker?
- [ ] Como provar que a VM nao bloqueia quando o worker atrasa?
- [ ] Qual sera o primeiro backend real: desktop `Gfx`/pixels, framebuffer local, ou fake backend?
### 1. Onde vive o worker controller?
**Pergunta:** O worker controller vive em `prometeu-system`, `prometeu-hal`, `prometeu-drivers`, ou host?
**Sugestao:** colocar o controller contratual em `prometeu-system`, proximo de `RenderManager`, e manter traits pequenas em `prometeu-hal` quando forem superficie compartilhada. `prometeu-drivers` deve continuar oferecendo implementacoes locais/testaveis; host desktop deve integrar o controller, nao definir o contrato.
**Motivo:** o `RenderManager` ja owns policy de AppMode, ownership/epoch, latest-wins e telemetry. Se o controller nascer apenas no host, a semantica de worker vira desktop-specific e fica mais dificil provar o contrato sem janela nativa.
### 2. Qual handoff thread-safe usar?
**Pergunta:** O handoff thread-safe sera `Mutex<Option<...>>`, canal bounded, atomic slot, ou estrutura propria?
**Sugestao:** comecar com uma estrutura propria simples baseada em `Mutex<Option<RenderSubmission>> + Condvar`, encapsulada como single-slot latest-wins. O produtor substitui a pending submission sem bloquear em rasterizacao; o consumidor espera por nova submission ou shutdown.
**Motivo:** canal bounded tende a preservar historico ou bloquear produtor se nao for usado com cuidado. Um slot explicito modela diretamente a regra ja aceita: nao existe fila crescente, a ultima submissao completa vence.
### 3. Como modelar current work, shutdown e stale owner?
**Pergunta:** Como modelar current work para shutdown e stale owner?
**Sugestao:** separar `pending` de `in_flight`. Ao tomar uma submission, o worker carimba `in_flight` localmente, renderiza, e checa ownership/epoch contra um snapshot atomico ou handle compartilhado antes de present. Shutdown sinaliza stop, acorda o worker e faz join bounded; se houver trabalho em andamento, ele pode terminar rasterizacao, mas nao pode apresentar se o owner/epoch estiver obsoleto ou o stop ja exigir descarte. O timeout deve ser configuravel; como default inicial, usar `250ms`, que e longo o bastante para evitar falso positivo em desktop comum e curto o bastante para detectar worker preso sem travar encerramento.
**Motivo:** isso evita que shutdown dependa de cancelar mid-raster em codigo que talvez nao seja cancelavel, mas ainda impede present obsoleto.
### 4. Quem chama present?
**Pergunta:** Quem chama present: worker, host event loop, ou display cadence service?
**Sugestao:** no primeiro corte, o worker deve produzir/publish uma superficie pronta como buffer RGBA8888 owned, por exemplo `OwnedRgba8888Frame`, definido em `prometeu-hal`. O host event loop continua sendo autoridade de apresentacao da janela nativa. O "repeat ultimo frame valido" deve repetir o ultimo buffer publicado, nao recompor nem exigir que o worker conheca a API nativa de janela/swap.
**Motivo:** RGBA8888 ja e o formato logico canonico nas specs. Um frame owned com `FrameId`, ownership/epoch, largura, altura, `stride_pixels` e pixels RGBA8888 em `Vec<u32>` deixa o contrato testavel sem janela e evita acoplamento a winit/pixels, SDL, swapchain ou textura nativa. Colocar o swap real dentro do worker pode funcionar em alguns hosts e quebrar portabilidade. O contrato importante e que a VM nao bloqueie e que o host tenha uma superficie publicada coerente.
### 5. Como o worker recebe recursos read-only?
**Pergunta:** Como o worker recebe recursos read-only preparados na `AGD-0042`?
**Sugestao:** o primeiro backend deve receber `RenderSubmission` owned e uma interface compacta de acesso read-only aos banks por id. Banks sao read-only por definicao durante o consumo de render; o worker resolve recursos por ids estaveis, sem copiar bank, sem snapshot de bank e sem depender de `Arc` como requisito do contrato.
**Motivo:** `AGD-0042` eliminou o bridge monolitico exatamente para impedir que o worker dependa do hardware inteiro. A proxima decisao precisa declarar quais recursos sao lidos pelo render backend e como sua estabilidade e garantida durante uma submissao em voo. A estabilidade vem da regra de leitura: o worker ve banks por id via interface read-only, enquanto instalacao/mutacao de bank permanece fora do consumo renderizavel em voo.
### 6. Qual erro tipado minimo?
**Pergunta:** Qual erro tipado minimo precisamos no primeiro worker?
**Sugestao:** introduzir um erro pequeno, por exemplo `RenderWorkerError`, cobrindo pelo menos: backend unavailable, render failed, present/publish failed, shutdown timeout, stale submission discarded. Panics continuam sendo capturados como falha interna, mas nao devem ser o modelo operacional normal.
**Motivo:** `LocalRenderWorker` hoje prova policy, mas nao diferencia falhas reais de render/present. O worker real precisa reportar falhas sem contaminar a VM com detalhes de host.
### 7. Como provar que a VM nao bloqueia?
**Pergunta:** Como provar que a VM nao bloqueia quando o worker atrasa?
**Sugestao:** criar testes deterministas com backend fake controlado por barreiras/condvars, nao sleeps. O teste deve segurar o worker em rasterizacao, produzir frames adicionais no runtime, verificar substituicao latest-wins/drop telemetry, e provar que o tick da VM retorna sem esperar o consumo terminar.
**Motivo:** sleeps tornam teste de concorrencia fragil. Barreiras permitem provar especificamente a propriedade desejada: produtor nao bloqueia em raster/present lento.
### 8. Qual primeiro backend real?
**Pergunta:** Qual sera o primeiro backend real: desktop `Gfx`/pixels, framebuffer local, ou fake backend?
**Sugestao:** implementar primeiro um backend fake/mocked para contrato e um backend local framebuffer para integracao sem janela. A integracao desktop vem em seguida como consumidor concreto, usando a mesma trait. Ao final desta agenda, o worker deve estar funcionando completamente no caminho host real; a divisao em planos serve para controlar risco, nao para reduzir o alvo final.
**Motivo:** se o primeiro backend for desktop/winit, o contrato corre risco de nascer acoplado a detalhes de janela. Se o primeiro for fake/local, a semantica do worker fica testavel antes da integracao visual. Ainda assim, a agenda so deve ser considerada concluida quando a integracao host real tambem estiver operando sobre o worker.
## Criterio para Encerrar
@ -149,5 +219,23 @@ Esta agenda pode virar decision quando tivermos:
## Discussion
- 2026-06-06: Agenda criada dentro da `DSC-0042` para separar worker real da preparacao de fronteira discutida em `AGD-0042`.
- 2026-06-15: Apos a conclusao dos planos `PLN-0098` a `PLN-0109`, a direcao da agenda foi alinhada em torno da Opcao C: controller em `prometeu-system`, handoff single-slot thread-safe, backend fake/local primeiro, e publicacao de frame owned RGBA8888 antes da integracao desktop concreta.
- 2026-06-15: Perguntas restantes alinhadas: `OwnedRgba8888Frame` deve viver em `prometeu-hal` com pixels `Vec<u32>` RGBA8888; banks sao acessados por interface compacta read-only via ids, sem copia/snapshot/`Arc` como contrato; shutdown deve ter timeout configuravel com default inicial sugerido de `250ms`; a agenda pode virar varios plans, mas so fecha quando o worker funcionar no host real.
## Resolution
Direcao proposta para decisao:
- `RenderManager` continua como autoridade de policy, ownership/epoch, latest-wins e telemetry;
- o worker controller contratual deve viver em `prometeu-system`;
- traits compartilhadas minimas podem viver em `prometeu-hal`;
- o handoff real deve ser single-slot latest-wins com sincronizacao explicita, inicialmente `Mutex<Option<RenderSubmission>> + Condvar`;
- o worker separa `pending` de `in_flight` e checa stop/ownership/epoch antes de publicar;
- shutdown deve ser bounded e configuravel; default inicial sugerido: `250ms`;
- o worker publica um `OwnedRgba8888Frame` em `prometeu-hal`, contendo frame id, owner/epoch, dimensoes, `stride_pixels` e pixels RGBA8888 owned em `Vec<u32>`;
- o worker acessa banks por ids estaveis atraves de uma interface compacta read-only; nao copia banks, nao depende de snapshots de bank e nao usa `Arc` como parte obrigatoria do contrato;
- o host event loop faz o upload/present nativo a partir do ultimo frame publicado;
- o primeiro backend deve ser fake/local para provar contrato e concorrencia sem janela;
- a integracao desktop concreta vem depois, usando a mesma trait/backend contract;
- a agenda pode ser executada em varios plans, mas o criterio final e ter o worker funcionando completamente no host real;
- testes devem usar barreiras/condvars para provar ausencia de bloqueio da VM, latest-wins, stale discard, shutdown bounded e telemetry.

View File

@ -0,0 +1,296 @@
---
id: DEC-0033
ticket: real-render-worker-establishment
title: Real Render Worker Contract
status: accepted
created: 2026-06-15
accepted:
agenda: AGD-0043
plans: [PLN-0110, PLN-0111, PLN-0112, PLN-0113, PLN-0114, PLN-0115, PLN-0116, PLN-0117, PLN-0118, PLN-0119, PLN-0120, PLN-0121]
tags: [runtime, renderer, worker, concurrency, host, hal, architecture]
---
## Status
Decision accepted from agenda `AGD-0043`.
This is the normative contract for implementation plans that establish the real render worker for Prometeu.
## Contexto
`DEC-0031` established the VM/render boundary around closed render submissions, single-slot handoff, frame pacing, AppMode policy, ownership epoch, and render telemetry.
`DEC-0032` and plans `PLN-0098` through `PLN-0109` prepared the platform boundary required for a real worker:
- `HardwareBridge` was removed from production/runtime contracts;
- `RuntimePlatform` became the runtime-facing platform service boundary;
- `RenderSubmissionSink` accepts owned render submissions;
- `Game2DFrameComposer` separates logical Game 2D frame closure from backend rendering;
- `gfx2d.*` and `gfxui.*` syscalls buffer commands instead of mutating `Gfx` immediately;
- firmware, Hub, runtime, host desktop, and tests moved to platform services;
- specs now state that worker/render consumers must not depend on `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state.
The remaining work is to replace the cooperative/local `LocalRenderWorker` proof with a real worker that drains render submissions without blocking VM progress and publishes complete RGBA8888 frames for host presentation.
## Decisao
Prometeu SHALL implement the real render worker as a runtime-owned worker controller with host-provided backend integration.
The worker controller SHALL live in `prometeu-system`, near `RenderManager`, because `RenderManager` owns AppMode policy, ownership/epoch, latest-wins handoff semantics, and render telemetry.
Shared contract types and traits SHALL live in `prometeu-hal` when they cross runtime/host/driver boundaries. Local/default and test implementations SHALL live in `prometeu-drivers` or host crates as appropriate.
The worker SHALL consume owned `RenderSubmission` values through a thread-safe single-slot latest-wins handoff. The VM producer MUST NOT block on rasterization, publication, presentation, or worker completion.
The worker SHALL publish owned RGBA8888 frames, not native window/swapchain objects. The initial shared frame type SHALL be `OwnedRgba8888Frame` in `prometeu-hal`.
The host event loop SHALL remain responsible for native window upload/present. The worker MUST NOT depend on winit, pixels, SDL, swapchain handles, native textures, or host window APIs.
The agenda may be implemented through multiple plans, but `AGD-0043` is complete only when the worker functions on the real host path, not merely in fake/local tests.
## Rationale
The real worker must prove the architecture promised by `DEC-0031`: VM execution and render consumption can proceed independently without crossing mutable runtime state into the renderer.
Putting the controller in `prometeu-system` keeps render policy in the same layer that already owns frame lifecycle, ownership epochs, latest-wins behavior, and telemetry. Putting it only in the host would make the contract desktop-specific and harder to test without a window.
Publishing an owned RGBA8888 frame keeps the worker host-agnostic. The host can upload that frame to any native presentation backend while the worker stays testable through ordinary memory comparisons.
Using a single explicit pending slot directly models the accepted backpressure rule. Channels or queues can accidentally preserve history or block producers. The runtime contract is not "render every produced frame"; it is "the latest complete submission wins and the VM does not wait for the consumer."
Read-only bank access by id preserves the no-copy resource boundary. Banks are render resources, not payloads to duplicate into each submission. The worker should resolve resource ids through a compact read-only interface without taking `&mut Hardware`, `&mut Gfx`, a live `FrameComposer`, or VM state.
## Invariantes / Contrato
### 1. Runtime controller ownership
The worker controller MUST live in `prometeu-system`.
`RenderManager` SHALL remain the authority for:
- AppMode render policy;
- ownership/epoch;
- latest-wins handoff semantics;
- render telemetry;
- stale submission discard;
- shutdown/request-stop coordination visible to runtime policy.
Host crates MAY instantiate or wire the controller, but they MUST NOT redefine worker semantics.
### 2. Shared HAL contracts
`prometeu-hal` SHALL define shared types/traits that cross the runtime/host/driver boundary.
At minimum, the worker work SHALL introduce:
- `OwnedRgba8888Frame`;
- compact read-only render resource access contracts for bank lookup by id;
- typed worker/backend error contracts where they are shared across crates.
### 3. Owned RGBA8888 frame publication
`OwnedRgba8888Frame` SHALL represent a completed logical frame ready for host upload/present.
It MUST include:
- `FrameId`;
- render ownership/epoch metadata;
- width;
- height;
- `stride_pixels`;
- owned RGBA8888 pixel data as `Vec<u32>`.
The pixel data MUST use the canonical RGBA8888 raw value contract already used by the runtime specs. It MUST NOT be described as native-endian host pixels, native texture data, or backend-specific surface memory.
Repeating the last valid frame SHALL mean reusing the last published `OwnedRgba8888Frame`, not recomposing from VM state or live composer state.
### 4. Thread-safe latest-wins handoff
The real handoff SHALL be a thread-safe single-slot latest-wins structure.
The initial implementation SHOULD use an explicit slot based on `Mutex<Option<RenderSubmission>> + Condvar`.
Producer behavior:
- publish replaces the pending submission if one already exists;
- replacement increments drop/replacement telemetry;
- publish MUST NOT wait for rasterization or present;
- publish MUST wake the consumer when needed.
Consumer behavior:
- wait for pending submission or shutdown;
- take ownership of the latest pending submission;
- leave the pending slot empty while work is in flight;
- discard stale work before publication if ownership/epoch no longer matches.
### 5. Pending and in-flight are separate
The worker MUST distinguish pending submissions from in-flight work.
Once the worker takes a submission, that submission is in flight. New producer submissions may replace the pending slot while the worker is still rasterizing the previous in-flight submission.
Before publishing an in-flight frame, the worker MUST check:
- stop/shutdown state;
- render owner;
- render epoch/generation.
If the frame is stale, the worker MUST discard it and update telemetry. It MUST NOT publish stale pixels.
### 6. Shutdown is bounded
Worker shutdown MUST be bounded and observable.
The shutdown timeout MUST be configurable. The initial default SHOULD be `250ms`.
On shutdown:
- stop state MUST be signaled;
- the worker MUST be woken if it is waiting;
- join MUST complete within the configured bound or return/report a `ShutdownTimeout` failure;
- in-flight work MAY finish rasterization, but MUST NOT publish after stop/epoch invalidation says it is stale.
### 7. Read-only bank access by id
The render backend SHALL resolve render resources through a compact read-only interface by stable ids.
Banks are read-only by definition during render consumption. The worker MUST NOT copy entire banks into submissions. The worker MUST NOT require bank snapshots. The worker MUST NOT require `Arc` as part of the public contract.
Implementation may use any internal storage strategy that satisfies the public contract, but the render worker contract is ids plus read-only access.
Resource installation, mutation, and residency changes remain outside in-flight render consumption.
### 8. Backend and host presentation split
The worker backend SHALL consume `RenderSubmission` plus read-only resource access and produce `OwnedRgba8888Frame`.
The host event loop SHALL upload/present the latest published frame through its native API.
The worker MUST NOT call native window present directly unless a future decision revises the host presentation model.
### 9. Typed error model
The worker work SHALL introduce a minimal typed error model, such as `RenderWorkerError`.
The error model MUST cover at least:
- backend unavailable;
- render failed;
- publish/present handoff failed;
- shutdown timeout;
- stale submission discarded;
- worker panic captured as internal failure.
Panic containment MAY remain as a safety net. Ordinary worker/backend failure MUST NOT rely on panic as the normal reporting path.
### 10. Telemetry
Worker telemetry SHALL preserve and extend the existing render telemetry model.
At minimum, telemetry SHOULD include:
- submissions produced;
- pending submissions replaced/dropped;
- submissions consumed;
- frames published/presented;
- stale submissions discarded;
- render failures;
- repeated-frame count;
- shutdown timeout count.
Telemetry MUST NOT change VM semantics.
### 11. Test contract
The worker contract MUST be proven without a native window.
Tests SHALL use deterministic synchronization primitives such as barriers and condvars, not timing sleeps as the primary proof mechanism.
The test matrix MUST prove:
- VM producer does not block when worker rasterization is held;
- latest-wins replacement occurs while a frame is in flight;
- stale owner/epoch prevents publication;
- shutdown is bounded;
- typed errors and telemetry are emitted;
- repeat uses the last published `OwnedRgba8888Frame`;
- fake/local backend behavior matches the contract before desktop integration.
### 12. Final scope
The implementation MAY be split into multiple plans.
However, the agenda is not complete until the real host path uses the worker. A fake/local backend is required for contract tests, but it is not sufficient as the final state of `AGD-0043`.
## Impactos
### Spec
Runtime specs must document:
- `OwnedRgba8888Frame`;
- the thread-safe single-slot latest-wins worker handoff;
- the worker/host presentation split;
- read-only bank access by id;
- shutdown and typed error behavior;
- telemetry expectations.
### Runtime
`prometeu-system` must add the worker controller and integrate it with `RenderManager`.
The VM tick path must submit render work without blocking on worker consumption.
### HAL
`prometeu-hal` must expose shared worker contracts:
- owned RGBA8888 frame type;
- render resource read-only access traits;
- worker/backend error types as needed.
### Drivers
`prometeu-drivers` must provide fake/local implementations suitable for deterministic tests and non-window integration.
Concrete local rendering may continue to use existing GFX machinery internally, but must expose the worker-facing contract without leaking `&mut Hardware`, `&mut Gfx`, or live `FrameComposer`.
### Host
The desktop host must integrate the real worker path and upload/present the latest published `OwnedRgba8888Frame` through its native window backend.
The host owns native presentation; the worker owns render consumption and frame publication.
### Tests
Tests must cover concurrency behavior deterministically and must include final host-path integration evidence.
## Referencias
- `AGD-0043`: Real Render Worker Establishment.
- `AGD-0042`: Host Hardware and Render Boundary Preparation.
- `DEC-0031`: VM and Render Parallel Execution Boundary.
- `DEC-0032`: Platform Layer and HardwareBridge Elimination.
- `LSN-0048`: Render Workers Need a Closed Packet Contract.
- `docs/specs/runtime/04-gfx-peripheral.md`.
- `docs/specs/runtime/11-portability-and-cross-platform-execution.md`.
## Propagacao Necessaria
Create implementation plans before editing specs or code.
Recommended plan sequence:
1. define `OwnedRgba8888Frame`, read-only bank access contracts, and worker error types in HAL;
2. introduce the thread-safe latest-wins handoff and deterministic fake/local worker tests;
3. implement worker controller lifecycle, stop token, bounded shutdown, pending/in-flight separation, and telemetry in `prometeu-system`;
4. implement fake/local backend that consumes `RenderSubmission` and produces `OwnedRgba8888Frame`;
5. integrate runtime render submission path with the worker without blocking VM ticks;
6. add stale owner/epoch discard and repeat-last-published-frame behavior;
7. integrate desktop host upload/present from the latest published `OwnedRgba8888Frame`;
8. update specs and final validation tests.
## Revision Log
- 2026-06-15: Initial decision draft generated from accepted `AGD-0043`.

View File

@ -2,7 +2,7 @@
id: PLN-0098
ticket: real-render-worker-establishment
title: Define Platform Facade Contracts
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0099
ticket: real-render-worker-establishment
title: Introduce Owned RenderSubmissionSink
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0100
ticket: real-render-worker-establishment
title: Migrate Local Render Publication to RenderSubmissionSink
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0101
ticket: real-render-worker-establishment
title: Remove Immediate Gfx2D and GfxUI Syscall Rendering
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0102
ticket: real-render-worker-establishment
title: Introduce Game2DFrameComposer Logical Service
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0103
ticket: real-render-worker-establishment
title: Introduce RuntimePlatform and TestPlatform
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0104
ticket: real-render-worker-establishment
title: Migrate VM Runtime HostContext to Platform Services
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0105
ticket: real-render-worker-establishment
title: Migrate Firmware and Hub to Platform Services
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0106
ticket: real-render-worker-establishment
title: Migrate Desktop Host to Platform Services
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0107
ticket: real-render-worker-establishment
title: Migrate Remaining Platform Domains
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0108
ticket: real-render-worker-establishment
title: Migrate Tests to TestPlatform
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -2,7 +2,7 @@
id: PLN-0109
ticket: real-render-worker-establishment
title: Remove HardwareBridge and Update Specs
status: open
status: done
created: 2026-06-06
completed:
ref_decisions: [DEC-0032]

View File

@ -0,0 +1,79 @@
---
id: PLN-0110
ticket: real-render-worker-establishment
title: Define Owned RGBA8888 Frame Contract
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, hal, frame-contract]
---
## Briefing
`DEC-0033` requires the worker to publish an owned RGBA8888 frame that is independent of native window APIs. This plan defines that shared HAL contract before any worker implementation depends on it.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Introduce `OwnedRgba8888Frame` in `prometeu-hal` as the canonical worker-to-host published frame type.
## Escopo
- Add the shared frame type to HAL.
- Include frame id, ownership/epoch metadata, dimensions, stride, and owned `Vec<u32>` RGBA8888 pixels.
- Add constructors and validation helpers that keep dimensions, stride, and pixel length coherent.
- Add unit tests for layout and validation.
## Fora de Escopo
- No worker thread.
- No host upload/present integration.
- No resource bank access.
- No spec updates beyond code-facing rustdoc if useful.
## Plano de Execucao
### Step 1 - Add HAL frame module
**What:** Add `OwnedRgba8888Frame`.
**How:** Create a HAL module, likely `crates/console/prometeu-hal/src/owned_frame.rs` or equivalent, and export it from `lib.rs`.
**File(s):** `crates/console/prometeu-hal/src/lib.rs`, new HAL frame module.
### Step 2 - Define required fields
**What:** Encode the DEC-0033 frame contract.
**How:** Include `FrameId`, render ownership/epoch metadata compatible with `RenderSubmission`, `width`, `height`, `stride_pixels`, and `pixels: Vec<u32>`.
**File(s):** HAL frame module, existing render ownership types if reuse is needed.
### Step 3 - Add constructors and validation
**What:** Prevent malformed frame buffers from entering the worker-host boundary.
**How:** Add checked constructor(s) that reject zero dimensions, too-small stride, and pixel buffers shorter than `stride_pixels * height`.
**File(s):** HAL frame module.
### Step 4 - Add tests
**What:** Pin layout semantics.
**How:** Test valid construction, invalid stride, invalid pixel length, and that raw pixel values are preserved as RGBA8888 `u32`.
**File(s):** HAL frame module tests.
## Criterios de Aceite
- [ ] `OwnedRgba8888Frame` is exported by `prometeu-hal`.
- [ ] The type contains `FrameId`, ownership/epoch, width, height, `stride_pixels`, and `Vec<u32>` pixels.
- [ ] Invalid dimensions/stride/pixel length cannot be silently accepted by checked constructors.
- [ ] No native host/window/pixels/winit/SDL type appears in the HAL frame contract.
## Tests / Validacao
- `cargo test -p prometeu-hal`
- `rg "OwnedRgba8888Frame" crates/console/prometeu-hal -n`
## Riscos
- Reusing native pixel terminology could blur the logical RGBA8888 contract.
- Overfitting the frame type to current desktop upload code would leak host details into HAL.

View File

@ -0,0 +1,79 @@
---
id: PLN-0111
ticket: real-render-worker-establishment
title: Define Read-Only Render Resource Access
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, hal, resources]
---
## Briefing
`DEC-0033` requires the worker backend to resolve render resources by stable ids through compact read-only access, without copying banks, snapshotting banks, requiring `Arc` in the public contract, or accessing mutable `Hardware`/`Gfx`/`FrameComposer`.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Define shared read-only render resource traits in HAL and implement them over existing local bank storage without changing bank ownership semantics.
## Escopo
- Define compact HAL traits for read-only glyph and scene bank access by id.
- Keep the public contract id-based and read-only.
- Implement the traits in the local driver/resource owner currently used by rendering.
- Add tests proving banks are read through the new interface.
## Fora de Escopo
- No worker thread.
- No bank copying or snapshotting.
- No public `Arc` requirement.
- No asset residency policy changes.
## Plano de Execucao
### Step 1 - Define HAL read-only traits
**What:** Add worker-facing resource access contracts.
**How:** Define traits for resolving glyph and scene banks by stable ids. Return borrowed read-only references or compact read-only views, not mutable handles.
**File(s):** `crates/console/prometeu-hal/src/*`, likely a new render resource module.
### Step 2 - Implement local resource access
**What:** Expose current local bank storage through the new traits.
**How:** Implement the read-only traits for the existing memory bank/resource owner used by `prometeu-drivers`.
**File(s):** `crates/console/prometeu-drivers/src/memory_banks.rs`, `frame_composer.rs`, or adjacent resource modules.
### Step 3 - Thread access through backend-facing APIs
**What:** Make future backends able to receive resource access without `Hardware`.
**How:** Add narrow parameters/types where needed so a backend can be given read-only resources independently from the full platform.
**File(s):** HAL contracts and driver modules touched above.
### Step 4 - Add resource access tests
**What:** Prove id lookup and read-only behavior.
**How:** Add tests for resident/missing glyph and scene banks through the new traits and verify no mutable API is exposed.
**File(s):** HAL/driver tests.
## Criterios de Aceite
- [ ] Worker-facing render resource access is defined in `prometeu-hal`.
- [ ] Resource access is by stable id and read-only.
- [ ] The public contract does not require `Arc`, bank copies, bank snapshots, `Hardware`, `Gfx`, or `FrameComposer`.
- [ ] Local driver storage implements the traits.
## Tests / Validacao
- `cargo test -p prometeu-hal -p prometeu-drivers`
- `rg "Arc<.*RenderResource|snapshot.*bank|&mut Hardware|&mut Gfx|FrameComposer" crates/console/prometeu-hal crates/console/prometeu-drivers -n`
## Riscos
- Returning concrete bank internals may overexpose driver implementation.
- Accidentally requiring `Arc` in the trait would make a storage strategy part of the public contract.

View File

@ -0,0 +1,78 @@
---
id: PLN-0112
ticket: real-render-worker-establishment
title: Define Render Worker Errors and Telemetry
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, hal, telemetry]
---
## Briefing
`DEC-0033` requires typed worker/backend errors and telemetry that observes worker behavior without changing VM semantics.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Introduce the shared error and telemetry vocabulary required by the real worker.
## Escopo
- Define `RenderWorkerError` or equivalent shared error type.
- Define worker telemetry counters/snapshot shape.
- Integrate the new telemetry shape with existing render telemetry types where appropriate.
- Add tests for error/debug/copy/equality behavior and telemetry defaults.
## Fora de Escopo
- No worker lifecycle implementation.
- No thread-safe handoff.
- No host integration.
## Plano de Execucao
### Step 1 - Add worker error type
**What:** Define typed worker errors.
**How:** Add variants for backend unavailable, render failed, publish/present handoff failed, shutdown timeout, stale submission discarded, and worker panic/internal failure.
**File(s):** `crates/console/prometeu-hal/src/*` or `prometeu-system` if the type is not shared; prefer HAL if host/tests need it.
### Step 2 - Add telemetry snapshot
**What:** Define counters required by DEC-0033.
**How:** Include produced, replaced/dropped, consumed, published/presented, stale discarded, render failures, repeated frames, and shutdown timeouts.
**File(s):** existing telemetry/render manager modules or new worker telemetry module.
### Step 3 - Preserve existing telemetry compatibility
**What:** Avoid breaking existing render telemetry tests.
**How:** Map or extend current render telemetry without changing VM semantics or current counters unexpectedly.
**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs`, HAL telemetry modules as needed.
### Step 4 - Add tests
**What:** Pin type behavior.
**How:** Test default zero telemetry, counter increments through helper methods if added, and error formatting/debug behavior.
**File(s):** HAL/system tests.
## Criterios de Aceite
- [ ] Worker/backend failure has a typed error surface.
- [ ] Telemetry includes all DEC-0033 minimum counters.
- [ ] Existing render telemetry tests still pass.
- [ ] No worker/backend failure path relies on panic as the normal reporting model.
## Tests / Validacao
- `cargo test -p prometeu-hal -p prometeu-system`
- `rg "RenderWorkerError|shutdown_timeout|stale.*discard|repeat" crates/console -n`
## Riscos
- Splitting telemetry between too many structs may make final worker accounting inconsistent.
- Adding errors before call sites exist may create unused-code warnings if not scoped carefully.

View File

@ -0,0 +1,80 @@
---
id: PLN-0113
ticket: real-render-worker-establishment
title: Implement Thread-Safe Latest-Wins Handoff
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, concurrency, handoff]
---
## Briefing
`DEC-0033` requires a thread-safe single-slot latest-wins handoff. The producer must replace pending work without waiting for rasterization or present, and the consumer must take ownership of the latest pending submission.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Add the real worker handoff primitive in `prometeu-system`, using explicit synchronization and deterministic tests.
## Escopo
- Implement a single-slot pending submission structure.
- Use `Mutex<Option<RenderSubmission>> + Condvar` or an equivalent explicit structure.
- Add producer publish/replace behavior and consumer wait/take behavior.
- Track replacement/drop telemetry.
## Fora de Escopo
- No worker thread controller lifecycle.
- No backend rendering.
- No host integration.
- No stale epoch publish behavior beyond storing ownership data already present in submissions.
## Plano de Execucao
### Step 1 - Add handoff module
**What:** Create the worker handoff primitive.
**How:** Add a module under `crates/console/prometeu-system/src/services/vm_runtime/`, for example `render_worker_handoff.rs`.
**File(s):** new system module, `mod.rs` exports.
### Step 2 - Implement publish replacement
**What:** Producer publishes owned `RenderSubmission`.
**How:** Lock the slot, replace any pending submission, increment replacement/drop counters when applicable, notify the condvar, and return immediately.
**File(s):** handoff module.
### Step 3 - Implement consumer wait/take
**What:** Consumer waits for work or shutdown signal.
**How:** Add wait/take APIs that block only the consumer side and return owned submissions.
**File(s):** handoff module.
### Step 4 - Add deterministic tests
**What:** Prove latest-wins and blocking boundaries.
**How:** Use threads, barriers, and condvars to prove producer replacement and consumer wake behavior without sleeps.
**File(s):** handoff module tests or `async_render_contract_tests.rs`.
## Criterios de Aceite
- [ ] Producer publish does not wait for consumer rasterization.
- [ ] Multiple pending publishes leave only the newest submission available.
- [ ] Replacements increment telemetry.
- [ ] Consumer can wait for work and is woken by publish.
- [ ] Tests do not use sleep as their primary synchronization proof.
## Tests / Validacao
- `cargo test -p prometeu-system render_worker_handoff`
- `cargo test -p prometeu-system`
## Riscos
- Holding the mutex while doing heavy work would violate non-blocking producer behavior.
- Condvar wait loops must handle spurious wakeups.

View File

@ -0,0 +1,78 @@
---
id: PLN-0114
ticket: real-render-worker-establishment
title: Add Deterministic Render Worker Test Harness
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, tests, concurrency]
---
## Briefing
`DEC-0033` requires worker behavior to be proven without a native window and without fragile sleeps. This plan creates reusable deterministic test fixtures for the remaining worker plans.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Add a fake backend and synchronization harness capable of blocking render at controlled points, observing publication, and proving non-blocking VM/producer behavior.
## Escopo
- Create test-only synchronization helpers using barriers/condvars.
- Create a fake render backend shape that can block/fail/publish deterministically.
- Add helper functions for constructing owned render submissions and frames.
- Reuse the harness in later worker tests.
## Fora de Escopo
- No production worker lifecycle.
- No desktop host integration.
- No real GFX rasterization.
## Plano de Execucao
### Step 1 - Add test support module
**What:** Introduce deterministic worker test fixtures.
**How:** Add test-only helpers under `prometeu-system` worker tests or a local test module.
**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/*`.
### Step 2 - Add controllable fake backend
**What:** Provide backend behavior for tests.
**How:** Fake backend can block at render start, unblock by test signal, return errors, and record published frames.
**File(s):** worker test module.
### Step 3 - Add submission/frame builders
**What:** Make tests concise and consistent.
**How:** Build Game/Shell submissions with explicit frame ids and ownership metadata; build `OwnedRgba8888Frame` fixtures.
**File(s):** worker test module.
### Step 4 - Add initial harness self-tests
**What:** Prove harness itself is deterministic.
**How:** Verify a test can block and release backend render without timing sleeps.
**File(s):** worker test module.
## Criterios de Aceite
- [ ] Tests can block worker backend render using deterministic synchronization.
- [ ] Tests can observe published `OwnedRgba8888Frame` values.
- [ ] Harness can inject backend errors.
- [ ] Harness helpers are scoped to tests or clearly internal.
## Tests / Validacao
- `cargo test -p prometeu-system render_worker`
- `rg "sleep\\(|thread::sleep" crates/console/prometeu-system/src/services/vm_runtime -n`
## Riscos
- Test harness code can become too coupled to implementation internals.
- Overly broad helpers can hide important assertions in later plans.

View File

@ -0,0 +1,87 @@
---
id: PLN-0115
ticket: real-render-worker-establishment
title: Implement Render Worker Controller Lifecycle
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, lifecycle, concurrency]
---
## Briefing
`DEC-0033` requires a runtime-owned worker controller in `prometeu-system`, with stop signaling, bounded shutdown, pending/in-flight separation, typed errors, and telemetry.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Implement the production worker controller lifecycle around the handoff primitive and backend trait shape.
## Escopo
- Add worker controller type in `prometeu-system`.
- Spawn and join a worker thread.
- Add configurable shutdown timeout with initial default `250ms`.
- Add stop token/wakeup behavior.
- Preserve pending/in-flight separation.
- Capture panic as typed internal failure.
## Fora de Escopo
- No desktop host presentation.
- No full runtime tick integration.
- No stale epoch/repeat behavior beyond lifecycle foundations.
## Plano de Execucao
### Step 1 - Define controller and config
**What:** Add worker controller and configuration.
**How:** Include shutdown timeout, default `250ms`, and hooks for backend and handoff dependencies.
**File(s):** new `render_worker.rs` or similar in `prometeu-system`.
### Step 2 - Implement start and run loop
**What:** Start worker execution.
**How:** Spawn a thread that waits on the handoff, takes submissions, marks in-flight locally, calls backend, and publishes results through a frame sink abstraction.
**File(s):** worker controller module.
### Step 3 - Implement stop and bounded join
**What:** Make shutdown observable and bounded.
**How:** Signal stop, wake the condvar, join with timeout strategy available in Rust implementation, and return/report `ShutdownTimeout` when exceeded.
**File(s):** worker controller module.
### Step 4 - Add panic containment
**What:** Convert worker panic into typed failure.
**How:** Use panic containment around backend calls or worker thread join result and update telemetry/errors.
**File(s):** worker controller module.
### Step 5 - Add lifecycle tests
**What:** Prove start/stop/join behavior.
**How:** Use deterministic harness from `PLN-0114`.
**File(s):** worker tests.
## Criterios de Aceite
- [ ] Worker controller lives in `prometeu-system`.
- [ ] Controller can start and stop a real worker thread.
- [ ] Shutdown wakes waiting worker and completes within configured bound in tests.
- [ ] Shutdown timeout is typed and observable.
- [ ] Worker panic is captured as typed internal failure.
## Tests / Validacao
- `cargo test -p prometeu-system render_worker`
- `cargo test -p prometeu-system`
## Riscos
- Rust thread join timeout requires careful design because standard `JoinHandle::join` has no timeout.
- Backend ownership must be `'static + Send` without leaking host-specific types into system contracts.

View File

@ -0,0 +1,78 @@
---
id: PLN-0116
ticket: real-render-worker-establishment
title: Implement Fake Local Render Backend
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, backend, drivers]
---
## Briefing
`DEC-0033` requires fake/local backend behavior before desktop integration so worker semantics can be proven without a window.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Implement backend contracts that consume `RenderSubmission` plus read-only resources and produce `OwnedRgba8888Frame`.
## Escopo
- Define or finalize a backend trait for worker rendering.
- Add a fake backend for deterministic tests.
- Add a local framebuffer backend that can rasterize through existing driver/GFX logic without exposing mutable `Hardware` as the worker contract.
- Verify output frame ownership and pixels.
## Fora de Escopo
- No desktop upload/present.
- No runtime tick integration.
- No resource mutation policy changes.
## Plano de Execucao
### Step 1 - Define backend trait
**What:** Encode backend consumption contract.
**How:** Trait consumes `RenderSubmission` and read-only resource access and returns `OwnedRgba8888Frame` or `RenderWorkerError`.
**File(s):** HAL if shared, or system if internal; prefer HAL if host/drivers implement it.
### Step 2 - Implement fake backend
**What:** Provide test backend.
**How:** Fake backend returns deterministic frames, can block through harness, and can return typed errors.
**File(s):** system test module or driver test utility.
### Step 3 - Implement local framebuffer backend
**What:** Provide non-window raster backend.
**How:** Use existing GFX rendering internally to turn submissions into `OwnedRgba8888Frame`, but expose only the backend trait and read-only resources.
**File(s):** `prometeu-drivers` rendering modules, possibly a new backend module.
### Step 4 - Add output tests
**What:** Prove frame content and metadata.
**How:** Render simple Game2D and ShellUi submissions and compare `OwnedRgba8888Frame` pixels/metadata.
**File(s):** driver/system tests.
## Criterios de Aceite
- [ ] Backend trait does not expose mutable `Hardware`, `Gfx`, `FrameComposer`, or VM state.
- [ ] Fake backend supports deterministic blocking/error behavior.
- [ ] Local backend produces `OwnedRgba8888Frame`.
- [ ] Game2D and ShellUi submissions can produce frame output without a native window.
## Tests / Validacao
- `cargo test -p prometeu-drivers -p prometeu-system`
- `rg "&mut Hardware|&mut Gfx|FrameComposer" crates/console/prometeu-hal crates/console/prometeu-system/src/services/vm_runtime -n`
## Riscos
- Local backend may be tempted to reuse `Hardware` directly as the public worker contract.
- Resource access gaps may surface when moving from fake to local rasterization.

View File

@ -0,0 +1,80 @@
---
id: PLN-0117
ticket: real-render-worker-establishment
title: Integrate Runtime Submission With Worker
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, integration]
---
## Briefing
`DEC-0033` requires VM/render production to submit work without blocking on worker consumption. This plan connects runtime frame closure to the worker handoff while preserving local synchronous fallback.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Route eligible Game render submissions from `RenderManager` into the real worker handoff and keep Shell/local paths correct.
## Escopo
- Add worker-capable runtime path.
- Submit owned `RenderSubmission` to the handoff.
- Preserve AppMode policy: Game may use worker; Shell remains local unless a future decision changes it.
- Keep local synchronous fallback.
- Prove VM tick returns without waiting for worker backend completion.
## Fora de Escopo
- No desktop host presentation.
- No final stale/repeat behavior beyond integration needed for basic worker use.
- No spec edits.
## Plano de Execucao
### Step 1 - Add worker ownership to runtime state
**What:** Store worker controller/handoff in runtime-owned state.
**How:** Extend `VirtualMachineRuntime` or `RenderManager` integration points without moving policy to host.
**File(s):** `crates/console/prometeu-system/src/services/vm_runtime/tick.rs`, `render_manager.rs`, worker modules.
### Step 2 - Submit Game frames to worker handoff
**What:** Replace local worker prototype path with real worker submission.
**How:** On eligible Game frame closure, publish owned `RenderSubmission` into the handoff and return to VM tick without waiting for consumption.
**File(s):** runtime tick/render manager integration.
### Step 3 - Preserve local fallback
**What:** Keep current local synchronous behavior available.
**How:** Use existing `RenderConsumerPath`/policy or update it to select local fallback when worker is disabled/unavailable.
**File(s):** `render_manager.rs`, `tick.rs`.
### Step 4 - Add non-blocking tests
**What:** Prove VM producer does not block.
**How:** Use harness to hold backend render, run tick/publish additional frames, assert return and replacement telemetry.
**File(s):** runtime worker tests.
## Criterios de Aceite
- [ ] Game render submissions can be sent to the real worker handoff.
- [ ] VM tick does not wait for worker rasterization.
- [ ] Shell/local policy remains unchanged.
- [ ] Local synchronous fallback still passes existing tests.
- [ ] Replacement telemetry increments when producer outruns worker.
## Tests / Validacao
- `cargo test -p prometeu-system`
- `cargo test -p prometeu-firmware`
## Riscos
- Accidentally making frame closure wait for worker consumption would violate DEC-0033.
- Mixing Shell lifecycle rendering into the worker path too early would broaden scope.

View File

@ -0,0 +1,85 @@
---
id: PLN-0118
ticket: real-render-worker-establishment
title: Add Stale Epoch and Repeat Frame Behavior
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, ownership, telemetry]
---
## Briefing
`DEC-0033` requires the worker to discard stale in-flight frames before publication and repeat the last published `OwnedRgba8888Frame` rather than recomposing.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Complete ownership/epoch checks and repeat-last-published-frame behavior for the worker path.
## Escopo
- Add shared active ownership/epoch observation for worker publication checks.
- Discard stale in-flight frames before publication.
- Store and expose the latest published `OwnedRgba8888Frame`.
- Implement repeat-last-frame behavior and telemetry.
## Fora de Escopo
- No desktop upload/present.
- No new AppMode policy.
- No native window invalidation changes.
## Plano de Execucao
### Step 1 - Expose ownership/epoch to worker
**What:** Let worker validate in-flight submissions before publishing.
**How:** Add a thread-safe snapshot or handle that the worker can read without mutating runtime state.
**File(s):** `render_manager.rs`, worker modules.
### Step 2 - Implement stale discard
**What:** Prevent stale pixels from publication.
**How:** Before publishing frame output, compare submission ownership/epoch with active ownership/epoch; discard on mismatch and increment telemetry.
**File(s):** worker controller module.
### Step 3 - Store latest published frame
**What:** Make the latest valid frame available to host/fallback tests.
**How:** Add a published-frame slot distinct from pending submission and in-flight work.
**File(s):** worker controller or published frame module.
### Step 4 - Implement repeat behavior
**What:** Repeat last valid frame without recomposition.
**How:** Add API to fetch/reuse the latest published `OwnedRgba8888Frame`; increment repeat telemetry when cadence requests reuse.
**File(s):** worker modules, tests.
### Step 5 - Add tests
**What:** Prove stale discard and repeat.
**How:** Use harness to hold in-flight work, change owner/epoch, release backend, and assert no publication; separately assert repeat returns last frame.
**File(s):** worker tests.
## Criterios de Aceite
- [ ] Worker checks active owner/epoch before publishing.
- [ ] Stale in-flight frames are discarded and counted.
- [ ] Latest published frame is an `OwnedRgba8888Frame`.
- [ ] Repeat uses the last published frame and does not recompose.
- [ ] Tests cover stale discard and repeat deterministically.
## Tests / Validacao
- `cargo test -p prometeu-system render_worker`
- `cargo test -p prometeu-system`
## Riscos
- Reading active ownership from the wrong layer may reintroduce mutable runtime coupling.
- Repeat behavior must not hide render failures unless telemetry records them.

View File

@ -0,0 +1,81 @@
---
id: PLN-0119
ticket: real-render-worker-establishment
title: Integrate Desktop Host Worker Presentation
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, host, desktop]
---
## Briefing
`DEC-0033` is not complete until the real host path uses the worker. This plan integrates desktop presentation with the latest published `OwnedRgba8888Frame` while keeping native upload/present in the host event loop.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Make `prometeu-host-desktop-winit` present frames produced by the worker path.
## Escopo
- Wire the host to enable/use the worker path.
- Read the latest published `OwnedRgba8888Frame`.
- Upload/copy RGBA8888 pixels to the existing pixels/winit presentation surface in the event loop.
- Preserve host invalidation and redraw behavior.
- Keep worker free of native window dependencies.
## Fora de Escopo
- No SDL migration.
- No change to logical pixel format.
- No change to input/audio/filesystem host behavior.
## Plano de Execucao
### Step 1 - Add host access to published frame
**What:** Expose latest worker-published frame to desktop host.
**How:** Add an API through runtime/platform integration that returns or copies the latest `OwnedRgba8888Frame` for host upload.
**File(s):** system worker modules, host runner integration.
### Step 2 - Upload RGBA8888 in event loop
**What:** Present worker output through existing host surface.
**How:** Convert/copy `Vec<u32>` RGBA8888 into the existing pixels frame buffer using existing RGBA8888 utility conventions.
**File(s):** `crates/host/prometeu-host-desktop-winit/src/runner.rs`, `utilities.rs` if needed.
### Step 3 - Preserve invalidation model
**What:** Keep host redraw policy coherent.
**How:** Trigger redraw when a new frame is published or host invalidation occurs; repeat uses the last published frame.
**File(s):** host runner/presentation state.
### Step 4 - Add host tests
**What:** Prove host consumes worker-published frame.
**How:** Add unit tests for RGBA8888 copy/upload helper and presentation state transitions without opening a native window.
**File(s):** host tests.
## Criterios de Aceite
- [ ] Desktop host presents the latest worker-published `OwnedRgba8888Frame`.
- [ ] Worker code does not import winit, pixels, SDL, swapchain, native texture, or window APIs.
- [ ] Host event loop remains responsible for native present.
- [ ] Existing host invalidation tests still pass.
- [ ] Real host path can run with worker enabled.
## Tests / Validacao
- `cargo test -p prometeu-host-desktop-winit`
- `cargo test -p prometeu-system -p prometeu-firmware`
- Manual run of a simple cartridge through desktop host with worker path enabled.
## Riscos
- Upload/copy can accidentally reinterpret RGBA8888 as native endian bytes.
- Host redraw policy can regress into perpetual polling if publication/invalidation are not kept separate.

View File

@ -0,0 +1,79 @@
---
id: PLN-0120
ticket: real-render-worker-establishment
title: Update Real Render Worker Specs
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, specs]
---
## Briefing
`DEC-0033` requires canonical runtime specs to document the real worker contract after implementation publishes the behavior.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Update English canonical specs to describe `OwnedRgba8888Frame`, worker handoff, host presentation split, read-only bank access, shutdown, errors, and telemetry.
## Escopo
- Update GFX/render specs.
- Update portability/host presentation specs.
- Update host ABI/syscall specs if worker behavior affects published runtime surfaces.
- Ensure no spec says the worker owns native window present.
## Fora de Escopo
- No new architecture beyond `DEC-0033`.
- No lesson writing.
- No implementation changes except documentation.
## Plano de Execucao
### Step 1 - Update GFX spec
**What:** Document worker frame publication.
**How:** Add `OwnedRgba8888Frame`, RGBA8888 `Vec<u32>` semantics, latest published frame, and repeat behavior.
**File(s):** `docs/specs/runtime/04-gfx-peripheral.md`.
### Step 2 - Update portability spec
**What:** Document worker/host split.
**How:** State that worker produces owned RGBA8888 frames and host event loop performs native upload/present.
**File(s):** `docs/specs/runtime/11-portability-and-cross-platform-execution.md`.
### Step 3 - Update concurrency/events spec if needed
**What:** Document handoff and shutdown behavior.
**How:** Add single-slot latest-wins, bounded shutdown, and non-blocking producer semantics where concurrency is specified.
**File(s):** `docs/specs/runtime/09-events-and-concurrency.md` or adjacent runtime spec.
### Step 4 - Update README/index references
**What:** Keep spec map discoverable.
**How:** Add any needed cross-reference to worker/render sections.
**File(s):** `docs/specs/runtime/README.md`.
## Criterios de Aceite
- [ ] Specs define `OwnedRgba8888Frame`.
- [ ] Specs state worker does not own native window present.
- [ ] Specs state producer does not block on worker rasterization/present.
- [ ] Specs document read-only bank access by id.
- [ ] Specs document bounded shutdown and typed error expectations.
## Tests / Validacao
- `rg "OwnedRgba8888Frame|latest-wins|shutdown|read-only" docs/specs/runtime -n`
- `discussion validate`
## Riscos
- Updating specs before implementation may accidentally publish intent as completed behavior; execute after code plans that establish the behavior.
- Spec wording must remain English and normative.

View File

@ -0,0 +1,102 @@
---
id: PLN-0121
ticket: real-render-worker-establishment
title: Final Worker Path Validation and Hardening
status: done
created: 2026-06-15
completed:
ref_decisions: [DEC-0033]
tags: [runtime, renderer, worker, validation, hardening]
---
## Briefing
`DEC-0033` is complete only when the worker functions on the real host path and the contract is proven end to end. This plan performs final validation, residue scans, and hardening before housekeeping.
## Decisions de Origem
- `DEC-0033`: Real Render Worker Contract.
## Alvo
Prove the real render worker path satisfies DEC-0033 across HAL, system, drivers, firmware, host, specs, and tests.
## Escopo
- Run broad test suites.
- Add missing edge tests found during validation.
- Scan for forbidden coupling.
- Verify host real path uses worker.
- Verify local fallback still works.
- Prepare evidence for discussion housekeeping.
## Fora de Escopo
- No new worker architecture.
- No SDL migration.
- No unrelated performance tuning.
## Plano de Execucao
### Step 1 - Run full affected test suite
**What:** Validate all touched crates.
**How:** Run workspace tests and focused worker/host tests.
**File(s):** repository root.
### Step 2 - Run coupling residue scans
**What:** Enforce DEC-0033 boundaries.
**How:** Search worker/system/HAL code for native window dependencies, mutable hardware/GFX/composer leaks, bank copying/snapshot terminology, and sleeps in concurrency tests.
**File(s):** repository root.
### Step 3 - Add missing hardening tests
**What:** Close any discovered gaps.
**How:** Add focused tests for missing telemetry/error/shutdown/host-path cases.
**File(s):** affected test modules.
### Step 4 - Verify host path
**What:** Prove desktop host uses worker output.
**How:** Run host tests and a manual/simple cartridge path with worker enabled; record evidence in final summary.
**File(s):** host runner and runtime integration.
### Step 5 - Prepare housekeeping evidence
**What:** Make `DSC-0042` ready for final lesson/housekeeping.
**How:** Record completed plan state and validation evidence for later `discussion-housekeep`.
**File(s):** discussion artifacts if needed.
## Criterios de Aceite
- [ ] `cargo test --workspace` passes.
- [ ] Focused worker, system, drivers, firmware, and host tests pass.
- [ ] Residue scans show worker code does not depend on native window APIs.
- [ ] Residue scans show worker boundary does not expose `&mut Hardware`, `&mut Gfx`, live `FrameComposer`, or mutable VM state.
- [ ] Desktop host path uses the real worker output.
- [ ] Local fallback still works.
- [ ] `discussion validate` passes.
## Tests / Validacao
- `cargo test --workspace`
- `cargo test -p prometeu-system -p prometeu-drivers -p prometeu-hal -p prometeu-firmware -p prometeu-host-desktop-winit`
- `discussion validate`
- `rg "winit|pixels|SDL|swapchain|native texture" crates/console/prometeu-system crates/console/prometeu-hal -n`
- `rg "&mut Hardware|&mut Gfx|FrameComposer" crates/console/prometeu-system/src/services/vm_runtime crates/console/prometeu-hal -n`
- `rg "thread::sleep|sleep\\(" crates/console/prometeu-system/src/services/vm_runtime -n`
## Riscos
- Final validation may expose earlier plan gaps; fix them in the narrowest affected plan/module.
- Manual host evidence can be flaky if tied to native window availability; keep automated host-unit evidence as the primary gate.
## Validation Evidence
- `cargo test --workspace`: passed.
- `cargo test -p prometeu-system -p prometeu-drivers -p prometeu-hal -p prometeu-firmware -p prometeu-host-desktop-winit`: passed.
- Native API coupling scan for worker/runtime console code: no `winit`, `pixels`, SDL, swapchain, or native texture API references found.
- Mutable boundary scan for worker/runtime console code: no `&mut Hardware`, `&mut Gfx`, or concrete live `FrameComposer` references found.
- Timing scan for render worker runtime tests: no `thread::sleep` / `sleep(` usage found.
- Added host-unit evidence that worker frame presentation is restricted to `GameRunning`; Shell/Hub paths keep local presentation behavior.

View File

@ -73,6 +73,24 @@ Color values in the runtime, HAL, host-facing framebuffer, and GFX ABI are
logical RGBA8888 values. RGB565 is not a supported runtime framebuffer,
palette, host presentation, or compatibility contract.
### Published owned frame
The canonical worker-published frame value is `OwnedRgba8888Frame`.
`OwnedRgba8888Frame` contains:
- the logical `FrameId`;
- render ownership metadata;
- width and height in pixels;
- stride in pixels;
- an owned `Vec<u32>` containing packed RGBA8888 pixels in canonical RGBA
channel order.
The pixel vector is owned by the published frame. Consumers may copy it into a
host upload buffer, retain it as the latest published frame, or repeat it for a
host redraw. Consumers MUST NOT interpret the frame as a native texture,
swapchain image, window handle, or host GPU resource.
---
## 3. Double Buffering
@ -134,6 +152,11 @@ to the logical/runtime side before handoff, or to an owning service. They MUST
NOT require the render consumer to hold mutable VM, `Hardware`, `Gfx`, or
`FrameComposer` references.
Read-only resource APIs expose compact lookup by ID. They do not expose mutable
bank state, `Arc` ownership as part of the contract, copies of whole banks, or
snapshot payloads. The implementation may choose its internal sharing mechanism,
but the render boundary contract is ID-based read-only access.
Local host implementations may keep a concrete hardware object internally as a
platform implementation detail. That object is not part of the runtime-facing
render boundary; the boundary is the typed platform service set.
@ -155,6 +178,12 @@ latest-wins semantics:
The consumer status is telemetry. It is not a semantic ACK to the VM.
The real render worker consumes owned `RenderSubmission` values from this
single-slot handoff and publishes owned `OwnedRgba8888Frame` values. The
producer path MUST remain non-blocking with respect to worker rasterization and
host present. A slow worker can cause replacement/drop telemetry, but it MUST
NOT create an unbounded queue or stall VM logical frame production.
### 4.4 Frame pacing
Game logical frames are paced by the runtime frame scheduler, not by render
@ -170,6 +199,10 @@ sequential. The render consumer may repeat the last valid frame, and telemetry
records the overrun/repeat. The VM MUST NOT produce catch-up frames to skip
from frame `N` to frame `N+k`.
Repeating a frame means reusing the latest published `OwnedRgba8888Frame`
without recomposing, rerunning VM code, or consuming a new submission. Repeat
behavior is presentation cadence behavior, not guest-visible execution.
### 4.5 AppMode policy
Render execution policy is explicit by pipeline/AppMode:
@ -231,6 +264,17 @@ Minimum render telemetry includes:
- last produced, consumed, presented, dropped, and error frame IDs;
- active render epoch.
### 4.8 Shutdown and typed failures
Render worker shutdown is bounded and observable. A shutdown request MUST
wake a waiting worker, discard pending submissions that will not be consumed,
and either join within the configured timeout or report a typed shutdown
failure.
Worker backend failures, sink publication failures, stale ownership discards,
panic capture, and shutdown timeout are typed render worker outcomes. They are
recorded through telemetry and diagnostics; they do not become VM-visible ACKs.
---
## 5. PROMETEU Graphical Structure

View File

@ -100,7 +100,34 @@ Important properties:
- no execution occurs outside the frame loop;
- frame structure remains observable for host tooling and host-owned certification.
## 7 Determinism and Best Practices
## 7 Render Worker Concurrency
The render worker is not a machine-visible event source and does not introduce
guest callbacks. It is an implementation-side consumer of closed render
submissions.
The render worker handoff uses single-slot latest-wins semantics:
- a producer publishes at most one pending owned `RenderSubmission`;
- a newer submission replaces an older unconsumed pending submission;
- replacement is counted as telemetry;
- the producer does not wait for worker rasterization or host present;
- the worker takes ownership of the submission it consumes.
The worker publishes `OwnedRgba8888Frame` values. Each published frame owns its
RGBA8888 pixel vector and carries frame and ownership metadata. Repeating a
frame reuses the latest published owned frame and does not execute guest code
or recompose the frame.
Render resources reachable from a submission are resolved through read-only
ID-based access. The worker MUST NOT hold mutable VM state, mutable `Hardware`,
mutable `Gfx`, or a live mutable `FrameComposer` as its cross-thread contract.
Shutdown is explicit and bounded. A shutdown request wakes a waiting worker,
causes pending work that will not be consumed to be discarded, and reports a
typed failure if the worker cannot join within the configured timeout.
## 8 Determinism and Best Practices
PROMETEU encourages:
@ -115,7 +142,7 @@ PROMETEU discourages:
- hidden timing channels;
- ambiguous out-of-band execution.
## 8 Relationship to Other Specs
## 9 Relationship to Other Specs
- [`09a-coroutines-and-cooperative-scheduling.md`](09a-coroutines-and-cooperative-scheduling.md) defines coroutine lifecycle and scheduling behavior.
- [`10-debug-inspection-and-profiling.md`](10-debug-inspection-and-profiling.md) defines observability and diagnostics surfaces.

View File

@ -118,6 +118,8 @@ Hardware differences:
The graphics system:
- produces typed logical render submissions that render to RGBA8888 output
- may consume Game submissions through a render worker that publishes owned
`OwnedRgba8888Frame` values
- uses an indexed palette
- does not depend on a specific GPU
@ -126,7 +128,8 @@ The platform layer:
- exposes typed service facades for render submission, render backend
execution, Game 2D composition, input, audio, assets, and telemetry
- consumes closed render submissions through a render-surface implementation
- transports published RGBA8888 output into a host presentation surface without
- transports published RGBA8888 output or worker-published
`OwnedRgba8888Frame` pixels into a host presentation surface without
injecting host-owned debug overlay pixels
- is the runtime-facing portability boundary; runtime and firmware code must not
depend on a monolithic hardware bridge or on a concrete hardware aggregate
@ -135,6 +138,12 @@ The host presentation layer MUST treat RGBA8888 as the canonical logical
framebuffer format. RGB565 conversion is not part of the normal host
presentation contract.
Native upload and present belong to the host event loop. The render worker MUST
NOT own or require a native window, swapchain, `pixels` surface, SDL texture, or
host GPU texture as part of its contract. The worker output is an owned
RGBA8888 frame; the host decides how to upload that frame to the native
presentation API for the current platform.
Host presentation SHOULD be driven by published render submissions and explicit
host-owned invalidation, not by perpetual redraw polling.

View File

@ -56,6 +56,13 @@ input, audio, assets, and telemetry. A concrete local hardware aggregate may
exist inside a host or test platform, but it is not the normative runtime-facing
contract.
The real render worker contract is documented across the GFX, events, and
portability chapters. `04-gfx-peripheral.md` defines the render submission and
`OwnedRgba8888Frame` publication contract, `09-events-and-concurrency.md`
defines latest-wins handoff and bounded shutdown behavior, and
`11-portability-and-cross-platform-execution.md` defines the split between
worker-owned RGBA8888 frame production and host-owned native upload/present.
## Document Functions
- `normative`: defines the technical contract, expected behavior, or implementation-facing surface.