Compare commits
18 Commits
9cf9fa6163
...
aa917cf44e
| Author | SHA1 | Date | |
|---|---|---|---|
| aa917cf44e | |||
| 850ca8691b | |||
| 67a629dbe4 | |||
| 6b5b27d068 | |||
| cfb5b1ac89 | |||
| 0c6a669e6e | |||
| 4b890850aa | |||
| 28105c2bf2 | |||
| 76d4e9c8ce | |||
| 57bf2b84d3 | |||
| 4c48b92f3b | |||
| 12d314a62d | |||
| 3916a652b2 | |||
| cafbc42018 | |||
| 1022750c12 | |||
| 373f842bef | |||
| a761e73587 | |||
| b4f7b4f3c0 |
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
162
crates/console/prometeu-drivers/src/local_render_backend.rs
Normal file
162
crates/console/prometeu-drivers/src/local_render_backend.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
127
crates/console/prometeu-hal/src/owned_frame.rs
Normal file
127
crates/console/prometeu-hal/src/owned_frame.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
109
crates/console/prometeu-hal/src/render_resources.rs
Normal file
109
crates/console/prometeu-hal/src/render_resources.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
203
crates/console/prometeu-hal/src/render_worker.rs
Normal file
203
crates/console/prometeu-hal/src/render_worker.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)]);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
@ -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.
|
||||
|
||||
@ -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`.
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user