Compare commits
No commits in common. "23d3ed641e75becdc21d2a8e58fcaaaf5b506454" and "bd147938a077fbf9e1e939c3c3a33e310e8a4b7d" have entirely different histories.
23d3ed641e
...
bd147938a0
File diff suppressed because it is too large
Load Diff
@ -112,12 +112,6 @@ pub enum LoadStatus {
|
|||||||
CANCELED = 4,
|
CANCELED = 4,
|
||||||
ERROR = 5,
|
ERROR = 5,
|
||||||
UnknownHandle = 6,
|
UnknownHandle = 6,
|
||||||
QUEUED = 7,
|
|
||||||
ACTIVE = 8,
|
|
||||||
SUPERSEDED = 9,
|
|
||||||
EMPTY = 10,
|
|
||||||
INVALID = 11,
|
|
||||||
BackendUnavailable = 12,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -135,36 +129,6 @@ pub enum AssetOpStatus {
|
|||||||
Ok = 0,
|
Ok = 0,
|
||||||
UnknownHandle = 1,
|
UnknownHandle = 1,
|
||||||
InvalidState = 2,
|
InvalidState = 2,
|
||||||
Superseded = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct AssetBacklogInfo {
|
|
||||||
pub status: AssetOpStatus,
|
|
||||||
pub pending_count: u32,
|
|
||||||
pub active_handle: HandleId,
|
|
||||||
pub active_asset_id: Option<AssetId>,
|
|
||||||
pub active_bank_type: Option<BankType>,
|
|
||||||
pub active_slot: Option<usize>,
|
|
||||||
pub active_progress: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct AssetBacklogPosition {
|
|
||||||
pub status: AssetOpStatus,
|
|
||||||
pub state: LoadStatus,
|
|
||||||
pub position: u32,
|
|
||||||
pub progress: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct AssetTargetStatus {
|
|
||||||
pub status: AssetOpStatus,
|
|
||||||
pub asset_id: Option<AssetId>,
|
|
||||||
pub handle: HandleId,
|
|
||||||
pub state: LoadStatus,
|
|
||||||
pub position: u32,
|
|
||||||
pub progress: u16,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
use crate::asset::{
|
use crate::asset::{
|
||||||
AssetBacklogInfo, AssetBacklogPosition, AssetEntry, AssetId, AssetLoadError, AssetOpStatus,
|
AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankTelemetry, HandleId, LoadStatus,
|
||||||
AssetTargetStatus, BankTelemetry, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef,
|
PreloadEntry, SlotRef, SlotStats,
|
||||||
SlotStats,
|
|
||||||
};
|
};
|
||||||
use crate::cartridge::AssetsPayloadSource;
|
use crate::cartridge::AssetsPayloadSource;
|
||||||
|
|
||||||
@ -16,12 +15,6 @@ pub trait AssetBridge {
|
|||||||
fn status(&self, handle: HandleId) -> LoadStatus;
|
fn status(&self, handle: HandleId) -> LoadStatus;
|
||||||
fn commit(&self, handle: HandleId) -> AssetOpStatus;
|
fn commit(&self, handle: HandleId) -> AssetOpStatus;
|
||||||
fn cancel(&self, handle: HandleId) -> AssetOpStatus;
|
fn cancel(&self, handle: HandleId) -> AssetOpStatus;
|
||||||
fn backlog_info(&self) -> AssetBacklogInfo;
|
|
||||||
fn backlog_position(&self, handle: HandleId) -> AssetBacklogPosition;
|
|
||||||
fn backlog_move(&self, handle: HandleId, new_position: usize) -> AssetOpStatus;
|
|
||||||
fn backlog_promote(&self, handle: HandleId) -> AssetOpStatus;
|
|
||||||
fn backlog_demote(&self, handle: HandleId) -> AssetOpStatus;
|
|
||||||
fn target_status(&self, bank_type: BankType, slot: usize) -> AssetTargetStatus;
|
|
||||||
fn apply_commits(&self);
|
fn apply_commits(&self);
|
||||||
fn bank_telemetry(&self) -> Vec<BankTelemetry>;
|
fn bank_telemetry(&self) -> Vec<BankTelemetry>;
|
||||||
fn slot_info(&self, slot: SlotRef) -> SlotStats;
|
fn slot_info(&self, slot: SlotRef) -> SlotStats;
|
||||||
|
|||||||
@ -69,12 +69,6 @@ pub enum Syscall {
|
|||||||
AssetStatus = 0x6002,
|
AssetStatus = 0x6002,
|
||||||
AssetCommit = 0x6003,
|
AssetCommit = 0x6003,
|
||||||
AssetCancel = 0x6004,
|
AssetCancel = 0x6004,
|
||||||
AssetBacklogInfo = 0x6005,
|
|
||||||
AssetBacklogPosition = 0x6006,
|
|
||||||
AssetBacklogMove = 0x6007,
|
|
||||||
AssetBacklogPromote = 0x6008,
|
|
||||||
AssetBacklogDemote = 0x6009,
|
|
||||||
AssetTargetStatus = 0x600A,
|
|
||||||
BankInfo = 0x6101,
|
BankInfo = 0x6101,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,40 +24,4 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
|||||||
.caps(caps::ASSET)
|
.caps(caps::ASSET)
|
||||||
.non_deterministic()
|
.non_deterministic()
|
||||||
.cost(20),
|
.cost(20),
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogInfo, "asset", "backlog_info")
|
|
||||||
.args(0)
|
|
||||||
.rets(7)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogPosition, "asset", "backlog_position")
|
|
||||||
.args(1)
|
|
||||||
.rets(4)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogMove, "asset", "backlog_move")
|
|
||||||
.args(2)
|
|
||||||
.rets(1)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(10),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogPromote, "asset", "backlog_promote")
|
|
||||||
.args(1)
|
|
||||||
.rets(1)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(10),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogDemote, "asset", "backlog_demote")
|
|
||||||
.args(1)
|
|
||||||
.rets(1)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(10),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetTargetStatus, "asset", "target_status")
|
|
||||||
.args(2)
|
|
||||||
.rets(6)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(5),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -53,12 +53,6 @@ impl Syscall {
|
|||||||
0x6002 => Some(Self::AssetStatus),
|
0x6002 => Some(Self::AssetStatus),
|
||||||
0x6003 => Some(Self::AssetCommit),
|
0x6003 => Some(Self::AssetCommit),
|
||||||
0x6004 => Some(Self::AssetCancel),
|
0x6004 => Some(Self::AssetCancel),
|
||||||
0x6005 => Some(Self::AssetBacklogInfo),
|
|
||||||
0x6006 => Some(Self::AssetBacklogPosition),
|
|
||||||
0x6007 => Some(Self::AssetBacklogMove),
|
|
||||||
0x6008 => Some(Self::AssetBacklogPromote),
|
|
||||||
0x6009 => Some(Self::AssetBacklogDemote),
|
|
||||||
0x600A => Some(Self::AssetTargetStatus),
|
|
||||||
0x6101 => Some(Self::BankInfo),
|
0x6101 => Some(Self::BankInfo),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@ -115,12 +109,6 @@ impl Syscall {
|
|||||||
Self::AssetStatus => "AssetStatus",
|
Self::AssetStatus => "AssetStatus",
|
||||||
Self::AssetCommit => "AssetCommit",
|
Self::AssetCommit => "AssetCommit",
|
||||||
Self::AssetCancel => "AssetCancel",
|
Self::AssetCancel => "AssetCancel",
|
||||||
Self::AssetBacklogInfo => "AssetBacklogInfo",
|
|
||||||
Self::AssetBacklogPosition => "AssetBacklogPosition",
|
|
||||||
Self::AssetBacklogMove => "AssetBacklogMove",
|
|
||||||
Self::AssetBacklogPromote => "AssetBacklogPromote",
|
|
||||||
Self::AssetBacklogDemote => "AssetBacklogDemote",
|
|
||||||
Self::AssetTargetStatus => "AssetTargetStatus",
|
|
||||||
Self::BankInfo => "BankInfo",
|
Self::BankInfo => "BankInfo",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -258,30 +258,6 @@ fn status_first_syscall_signatures_are_pinned() {
|
|||||||
assert_eq!(asset_cancel.arg_slots, 1);
|
assert_eq!(asset_cancel.arg_slots, 1);
|
||||||
assert_eq!(asset_cancel.ret_slots, 1);
|
assert_eq!(asset_cancel.ret_slots, 1);
|
||||||
|
|
||||||
let asset_backlog_info = meta_for(Syscall::AssetBacklogInfo);
|
|
||||||
assert_eq!(asset_backlog_info.arg_slots, 0);
|
|
||||||
assert_eq!(asset_backlog_info.ret_slots, 7);
|
|
||||||
|
|
||||||
let asset_backlog_position = meta_for(Syscall::AssetBacklogPosition);
|
|
||||||
assert_eq!(asset_backlog_position.arg_slots, 1);
|
|
||||||
assert_eq!(asset_backlog_position.ret_slots, 4);
|
|
||||||
|
|
||||||
let asset_backlog_move = meta_for(Syscall::AssetBacklogMove);
|
|
||||||
assert_eq!(asset_backlog_move.arg_slots, 2);
|
|
||||||
assert_eq!(asset_backlog_move.ret_slots, 1);
|
|
||||||
|
|
||||||
let asset_backlog_promote = meta_for(Syscall::AssetBacklogPromote);
|
|
||||||
assert_eq!(asset_backlog_promote.arg_slots, 1);
|
|
||||||
assert_eq!(asset_backlog_promote.ret_slots, 1);
|
|
||||||
|
|
||||||
let asset_backlog_demote = meta_for(Syscall::AssetBacklogDemote);
|
|
||||||
assert_eq!(asset_backlog_demote.arg_slots, 1);
|
|
||||||
assert_eq!(asset_backlog_demote.ret_slots, 1);
|
|
||||||
|
|
||||||
let asset_target_status = meta_for(Syscall::AssetTargetStatus);
|
|
||||||
assert_eq!(asset_target_status.arg_slots, 2);
|
|
||||||
assert_eq!(asset_target_status.ret_slots, 6);
|
|
||||||
|
|
||||||
let bank_info = meta_for(Syscall::BankInfo);
|
let bank_info = meta_for(Syscall::BankInfo);
|
||||||
assert_eq!(bank_info.arg_slots, 1);
|
assert_eq!(bank_info.arg_slots, 1);
|
||||||
assert_eq!(bank_info.ret_slots, 2);
|
assert_eq!(bank_info.ret_slots, 2);
|
||||||
|
|||||||
@ -7,13 +7,7 @@ pub use crash_report::CrashReport;
|
|||||||
pub use os::{LifecycleError, LifecycleOperation, SystemOS};
|
pub use os::{LifecycleError, LifecycleOperation, SystemOS};
|
||||||
pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
|
pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
|
||||||
pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink};
|
pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink};
|
||||||
pub use services::async_work::{
|
|
||||||
AsyncWorkActiveJob, AsyncWorkCancelToken, AsyncWorkJobContext, AsyncWorkJobId,
|
|
||||||
AsyncWorkJobKind, AsyncWorkJobOutcome, AsyncWorkLane, AsyncWorkLaneConfig,
|
|
||||||
AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority,
|
|
||||||
};
|
|
||||||
pub use services::fs;
|
pub use services::fs;
|
||||||
pub use services::memcard::MemcardAsyncLaneOperation;
|
|
||||||
pub use services::process;
|
pub use services::process;
|
||||||
pub use services::task;
|
pub use services::task;
|
||||||
pub use services::vm_runtime::{
|
pub use services::vm_runtime::{
|
||||||
|
|||||||
@ -1,601 +0,0 @@
|
|||||||
use std::collections::VecDeque;
|
|
||||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
|
|
||||||
use std::sync::{Arc, Condvar, Mutex};
|
|
||||||
use std::thread::{self, JoinHandle};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct AsyncWorkJobId(u64);
|
|
||||||
|
|
||||||
impl AsyncWorkJobId {
|
|
||||||
pub const ZERO: Self = Self(0);
|
|
||||||
|
|
||||||
pub fn new(value: u64) -> Self {
|
|
||||||
Self(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(self) -> u64 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum AsyncWorkJobKind {
|
|
||||||
Asset,
|
|
||||||
Memcard,
|
|
||||||
Fs,
|
|
||||||
Test,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum AsyncWorkPriority {
|
|
||||||
MemcardCommitWrite,
|
|
||||||
FsWriteConfig,
|
|
||||||
AssetLoad,
|
|
||||||
NonCriticalReadList,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncWorkPriority {
|
|
||||||
const fn rank(self) -> u8 {
|
|
||||||
match self {
|
|
||||||
Self::MemcardCommitWrite => 0,
|
|
||||||
Self::FsWriteConfig => 1,
|
|
||||||
Self::AssetLoad => 2,
|
|
||||||
Self::NonCriticalReadList => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncWorkJobKind {
|
|
||||||
pub const fn default_priority(self) -> AsyncWorkPriority {
|
|
||||||
match self {
|
|
||||||
Self::Memcard => AsyncWorkPriority::MemcardCommitWrite,
|
|
||||||
Self::Fs => AsyncWorkPriority::FsWriteConfig,
|
|
||||||
Self::Asset => AsyncWorkPriority::AssetLoad,
|
|
||||||
Self::Test => AsyncWorkPriority::NonCriticalReadList,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum AsyncWorkJobOutcome {
|
|
||||||
Completed,
|
|
||||||
Canceled,
|
|
||||||
Failed,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum AsyncWorkLaneError {
|
|
||||||
Shutdown,
|
|
||||||
WorkerPanic,
|
|
||||||
ShutdownTimeout,
|
|
||||||
InternalFailure,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct AsyncWorkLaneConfig {
|
|
||||||
pub shutdown_timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AsyncWorkLaneConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self { shutdown_timeout: Duration::from_millis(250) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct AsyncWorkActiveJob {
|
|
||||||
pub id: AsyncWorkJobId,
|
|
||||||
pub kind: AsyncWorkJobKind,
|
|
||||||
pub priority: AsyncWorkPriority,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
pub struct AsyncWorkLaneTelemetry {
|
|
||||||
pub submitted_jobs: u64,
|
|
||||||
pub started_jobs: u64,
|
|
||||||
pub completed_jobs: u64,
|
|
||||||
pub canceled_jobs: u64,
|
|
||||||
pub failed_jobs: u64,
|
|
||||||
pub shutdown_discards: u64,
|
|
||||||
pub pending_depth: usize,
|
|
||||||
pub max_pending_depth: usize,
|
|
||||||
pub active_job_id: Option<AsyncWorkJobId>,
|
|
||||||
pub last_submitted_job_id: Option<AsyncWorkJobId>,
|
|
||||||
pub last_started_job_id: Option<AsyncWorkJobId>,
|
|
||||||
pub last_closed_job_id: Option<AsyncWorkJobId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncWorkLaneTelemetry {
|
|
||||||
fn record_submitted(&mut self, job_id: AsyncWorkJobId, pending_depth: usize) {
|
|
||||||
self.submitted_jobs += 1;
|
|
||||||
self.pending_depth = pending_depth;
|
|
||||||
self.max_pending_depth = self.max_pending_depth.max(pending_depth);
|
|
||||||
self.last_submitted_job_id = Some(job_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_started(&mut self, job_id: AsyncWorkJobId, pending_depth: usize) {
|
|
||||||
self.started_jobs += 1;
|
|
||||||
self.pending_depth = pending_depth;
|
|
||||||
self.active_job_id = Some(job_id);
|
|
||||||
self.last_started_job_id = Some(job_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_closed(&mut self, job_id: AsyncWorkJobId, outcome: AsyncWorkJobOutcome) {
|
|
||||||
match outcome {
|
|
||||||
AsyncWorkJobOutcome::Completed => self.completed_jobs += 1,
|
|
||||||
AsyncWorkJobOutcome::Canceled => self.canceled_jobs += 1,
|
|
||||||
AsyncWorkJobOutcome::Failed => self.failed_jobs += 1,
|
|
||||||
}
|
|
||||||
self.active_job_id = None;
|
|
||||||
self.last_closed_job_id = Some(job_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_shutdown_discards(&mut self, count: usize) {
|
|
||||||
self.shutdown_discards += count as u64;
|
|
||||||
self.pending_depth = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct AsyncWorkCancelToken {
|
|
||||||
canceled: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncWorkCancelToken {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self { canceled: Arc::new(AtomicBool::new(false)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cancel(&self) {
|
|
||||||
self.canceled.store(true, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_canceled(&self) -> bool {
|
|
||||||
self.canceled.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct AsyncWorkJobContext {
|
|
||||||
pub job_id: AsyncWorkJobId,
|
|
||||||
pub kind: AsyncWorkJobKind,
|
|
||||||
pub priority: AsyncWorkPriority,
|
|
||||||
cancel_token: AsyncWorkCancelToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncWorkJobContext {
|
|
||||||
pub fn is_canceled(&self) -> bool {
|
|
||||||
self.cancel_token.is_canceled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type AsyncWorkFn = Box<dyn FnOnce(AsyncWorkJobContext) -> AsyncWorkJobOutcome + Send + 'static>;
|
|
||||||
|
|
||||||
struct PendingAsyncWorkJob {
|
|
||||||
id: AsyncWorkJobId,
|
|
||||||
kind: AsyncWorkJobKind,
|
|
||||||
priority: AsyncWorkPriority,
|
|
||||||
cancel_token: AsyncWorkCancelToken,
|
|
||||||
work: AsyncWorkFn,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RunningAsyncWorkJob {
|
|
||||||
id: AsyncWorkJobId,
|
|
||||||
kind: AsyncWorkJobKind,
|
|
||||||
priority: AsyncWorkPriority,
|
|
||||||
cancel_token: AsyncWorkCancelToken,
|
|
||||||
work: AsyncWorkFn,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct AsyncWorkLaneState {
|
|
||||||
next_job_id: u64,
|
|
||||||
pending: VecDeque<PendingAsyncWorkJob>,
|
|
||||||
active: Option<AsyncWorkActiveJob>,
|
|
||||||
active_cancel_token: Option<AsyncWorkCancelToken>,
|
|
||||||
shutdown_requested: bool,
|
|
||||||
telemetry: AsyncWorkLaneTelemetry,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct AsyncWorkLane {
|
|
||||||
state: Mutex<AsyncWorkLaneState>,
|
|
||||||
ready: Condvar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncWorkLane {
|
|
||||||
pub fn submit(
|
|
||||||
&self,
|
|
||||||
kind: AsyncWorkJobKind,
|
|
||||||
work: impl FnOnce(AsyncWorkJobContext) -> AsyncWorkJobOutcome + Send + 'static,
|
|
||||||
) -> Result<AsyncWorkJobId, AsyncWorkLaneError> {
|
|
||||||
self.submit_with_priority(kind, kind.default_priority(), work)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn submit_with_priority(
|
|
||||||
&self,
|
|
||||||
kind: AsyncWorkJobKind,
|
|
||||||
priority: AsyncWorkPriority,
|
|
||||||
work: impl FnOnce(AsyncWorkJobContext) -> AsyncWorkJobOutcome + Send + 'static,
|
|
||||||
) -> Result<AsyncWorkJobId, AsyncWorkLaneError> {
|
|
||||||
let mut state = self.state.lock().map_err(|_| AsyncWorkLaneError::InternalFailure)?;
|
|
||||||
if state.shutdown_requested {
|
|
||||||
return Err(AsyncWorkLaneError::Shutdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.next_job_id += 1;
|
|
||||||
let id = AsyncWorkJobId::new(state.next_job_id);
|
|
||||||
let cancel_token = AsyncWorkCancelToken::new();
|
|
||||||
let job = PendingAsyncWorkJob { id, kind, priority, cancel_token, work: Box::new(work) };
|
|
||||||
let insert_at = state
|
|
||||||
.pending
|
|
||||||
.iter()
|
|
||||||
.position(|pending| priority.rank() < pending.priority.rank())
|
|
||||||
.unwrap_or(state.pending.len());
|
|
||||||
state.pending.insert(insert_at, job);
|
|
||||||
let pending_depth = state.pending.len();
|
|
||||||
state.telemetry.record_submitted(id, pending_depth);
|
|
||||||
self.ready.notify_one();
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn submit_memcard_commit_write(
|
|
||||||
&self,
|
|
||||||
work: impl FnOnce(AsyncWorkJobContext) -> AsyncWorkJobOutcome + Send + 'static,
|
|
||||||
) -> Result<AsyncWorkJobId, AsyncWorkLaneError> {
|
|
||||||
self.submit_with_priority(
|
|
||||||
AsyncWorkJobKind::Memcard,
|
|
||||||
AsyncWorkPriority::MemcardCommitWrite,
|
|
||||||
work,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn submit_fs_write_config(
|
|
||||||
&self,
|
|
||||||
work: impl FnOnce(AsyncWorkJobContext) -> AsyncWorkJobOutcome + Send + 'static,
|
|
||||||
) -> Result<AsyncWorkJobId, AsyncWorkLaneError> {
|
|
||||||
self.submit_with_priority(AsyncWorkJobKind::Fs, AsyncWorkPriority::FsWriteConfig, work)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn submit_asset_load(
|
|
||||||
&self,
|
|
||||||
work: impl FnOnce(AsyncWorkJobContext) -> AsyncWorkJobOutcome + Send + 'static,
|
|
||||||
) -> Result<AsyncWorkJobId, AsyncWorkLaneError> {
|
|
||||||
self.submit_with_priority(AsyncWorkJobKind::Asset, AsyncWorkPriority::AssetLoad, work)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn submit_non_critical_read_list(
|
|
||||||
&self,
|
|
||||||
kind: AsyncWorkJobKind,
|
|
||||||
work: impl FnOnce(AsyncWorkJobContext) -> AsyncWorkJobOutcome + Send + 'static,
|
|
||||||
) -> Result<AsyncWorkJobId, AsyncWorkLaneError> {
|
|
||||||
self.submit_with_priority(kind, AsyncWorkPriority::NonCriticalReadList, work)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_cancel(&self, job_id: AsyncWorkJobId) -> bool {
|
|
||||||
let state = self.state.lock().unwrap();
|
|
||||||
if let Some(token) = state
|
|
||||||
.active
|
|
||||||
.filter(|active| active.id == job_id)
|
|
||||||
.and(state.active_cancel_token.as_ref())
|
|
||||||
{
|
|
||||||
token.cancel();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for pending in &state.pending {
|
|
||||||
if pending.id == job_id {
|
|
||||||
pending.cancel_token.cancel();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_job(&self) -> Option<AsyncWorkActiveJob> {
|
|
||||||
self.state.lock().unwrap().active
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pending_depth(&self) -> usize {
|
|
||||||
self.state.lock().unwrap().pending.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn telemetry(&self) -> AsyncWorkLaneTelemetry {
|
|
||||||
self.state.lock().unwrap().telemetry
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_shutdown(&self) {
|
|
||||||
let mut state = self.state.lock().unwrap();
|
|
||||||
state.shutdown_requested = true;
|
|
||||||
if let Some(token) = &state.active_cancel_token {
|
|
||||||
token.cancel();
|
|
||||||
}
|
|
||||||
self.ready.notify_all();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_take(&self) -> Option<RunningAsyncWorkJob> {
|
|
||||||
self.wait_take_with_hook(|| {})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_take_with_hook(&self, mut before_wait: impl FnMut()) -> Option<RunningAsyncWorkJob> {
|
|
||||||
let mut state = self.state.lock().unwrap();
|
|
||||||
loop {
|
|
||||||
if state.shutdown_requested {
|
|
||||||
let discarded = state.pending.len();
|
|
||||||
state.pending.clear();
|
|
||||||
state.telemetry.record_shutdown_discards(discarded);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(pending) = state.pending.pop_front() {
|
|
||||||
let active = AsyncWorkActiveJob {
|
|
||||||
id: pending.id,
|
|
||||||
kind: pending.kind,
|
|
||||||
priority: pending.priority,
|
|
||||||
};
|
|
||||||
state.active = Some(active);
|
|
||||||
state.active_cancel_token = Some(pending.cancel_token.clone());
|
|
||||||
let pending_depth = state.pending.len();
|
|
||||||
state.telemetry.record_started(pending.id, pending_depth);
|
|
||||||
return Some(RunningAsyncWorkJob {
|
|
||||||
id: pending.id,
|
|
||||||
kind: pending.kind,
|
|
||||||
priority: pending.priority,
|
|
||||||
cancel_token: pending.cancel_token,
|
|
||||||
work: pending.work,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
before_wait();
|
|
||||||
state = self.ready.wait(state).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_finished(&self, job_id: AsyncWorkJobId, outcome: AsyncWorkJobOutcome) {
|
|
||||||
let mut state = self.state.lock().unwrap();
|
|
||||||
if state.active.is_some_and(|active| active.id == job_id) {
|
|
||||||
state.active = None;
|
|
||||||
state.active_cancel_token = None;
|
|
||||||
}
|
|
||||||
state.telemetry.record_closed(job_id, outcome);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AsyncWorkLaneController {
|
|
||||||
config: AsyncWorkLaneConfig,
|
|
||||||
lane: Arc<AsyncWorkLane>,
|
|
||||||
handle: Option<JoinHandle<()>>,
|
|
||||||
done_rx: Receiver<Result<(), AsyncWorkLaneError>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncWorkLaneController {
|
|
||||||
pub fn start(config: AsyncWorkLaneConfig, lane: Arc<AsyncWorkLane>) -> Self {
|
|
||||||
let worker_lane = Arc::clone(&lane);
|
|
||||||
let (done_tx, done_rx) = mpsc::channel();
|
|
||||||
let handle = thread::spawn(move || {
|
|
||||||
let result = catch_unwind(AssertUnwindSafe(|| run_async_work_loop(worker_lane)))
|
|
||||||
.map_err(|_| AsyncWorkLaneError::WorkerPanic);
|
|
||||||
let _ = done_tx.send(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self { config, lane, handle: Some(handle), done_rx }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lane(&self) -> Arc<AsyncWorkLane> {
|
|
||||||
Arc::clone(&self.lane)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stop(&mut self) -> Result<(), AsyncWorkLaneError> {
|
|
||||||
self.lane.request_shutdown();
|
|
||||||
match self.done_rx.recv_timeout(self.config.shutdown_timeout) {
|
|
||||||
Ok(result) => {
|
|
||||||
if let Some(handle) = self.handle.take()
|
|
||||||
&& handle.join().is_err()
|
|
||||||
{
|
|
||||||
return Err(AsyncWorkLaneError::WorkerPanic);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
Err(RecvTimeoutError::Timeout) => Err(AsyncWorkLaneError::ShutdownTimeout),
|
|
||||||
Err(RecvTimeoutError::Disconnected) => Err(AsyncWorkLaneError::InternalFailure),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for AsyncWorkLaneController {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.handle.is_some() {
|
|
||||||
let _ = self.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_async_work_loop(lane: Arc<AsyncWorkLane>) {
|
|
||||||
while let Some(job) = lane.wait_take() {
|
|
||||||
let context = AsyncWorkJobContext {
|
|
||||||
job_id: job.id,
|
|
||||||
kind: job.kind,
|
|
||||||
priority: job.priority,
|
|
||||||
cancel_token: job.cancel_token,
|
|
||||||
};
|
|
||||||
let result = catch_unwind(AssertUnwindSafe(|| (job.work)(context)))
|
|
||||||
.unwrap_or(AsyncWorkJobOutcome::Failed);
|
|
||||||
lane.record_finished(job.id, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::sync::mpsc;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn async_work_lane_runs_jobs_in_submission_order() {
|
|
||||||
let lane = Arc::new(AsyncWorkLane::default());
|
|
||||||
let mut controller =
|
|
||||||
AsyncWorkLaneController::start(AsyncWorkLaneConfig::default(), Arc::clone(&lane));
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
|
|
||||||
lane.submit(AsyncWorkJobKind::Test, {
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |_| {
|
|
||||||
tx.send(1).unwrap();
|
|
||||||
AsyncWorkJobOutcome::Completed
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
lane.submit(AsyncWorkJobKind::Test, {
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |_| {
|
|
||||||
tx.send(2).unwrap();
|
|
||||||
AsyncWorkJobOutcome::Completed
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(rx.recv().unwrap(), 1);
|
|
||||||
assert_eq!(rx.recv().unwrap(), 2);
|
|
||||||
controller.stop().unwrap();
|
|
||||||
|
|
||||||
let telemetry = lane.telemetry();
|
|
||||||
assert_eq!(telemetry.submitted_jobs, 2);
|
|
||||||
assert_eq!(telemetry.started_jobs, 2);
|
|
||||||
assert_eq!(telemetry.completed_jobs, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn async_work_lane_runs_higher_priority_io_before_assets() {
|
|
||||||
let lane = Arc::new(AsyncWorkLane::default());
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
|
|
||||||
lane.submit_asset_load({
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |ctx| {
|
|
||||||
tx.send((ctx.kind, ctx.priority)).unwrap();
|
|
||||||
AsyncWorkJobOutcome::Completed
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
lane.submit_non_critical_read_list(AsyncWorkJobKind::Fs, {
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |ctx| {
|
|
||||||
tx.send((ctx.kind, ctx.priority)).unwrap();
|
|
||||||
AsyncWorkJobOutcome::Completed
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
lane.submit_memcard_commit_write({
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |ctx| {
|
|
||||||
tx.send((ctx.kind, ctx.priority)).unwrap();
|
|
||||||
AsyncWorkJobOutcome::Completed
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
lane.submit_fs_write_config({
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |ctx| {
|
|
||||||
tx.send((ctx.kind, ctx.priority)).unwrap();
|
|
||||||
AsyncWorkJobOutcome::Completed
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut controller =
|
|
||||||
AsyncWorkLaneController::start(AsyncWorkLaneConfig::default(), Arc::clone(&lane));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rx.recv().unwrap(),
|
|
||||||
(AsyncWorkJobKind::Memcard, AsyncWorkPriority::MemcardCommitWrite)
|
|
||||||
);
|
|
||||||
assert_eq!(rx.recv().unwrap(), (AsyncWorkJobKind::Fs, AsyncWorkPriority::FsWriteConfig));
|
|
||||||
assert_eq!(rx.recv().unwrap(), (AsyncWorkJobKind::Asset, AsyncWorkPriority::AssetLoad));
|
|
||||||
assert_eq!(
|
|
||||||
rx.recv().unwrap(),
|
|
||||||
(AsyncWorkJobKind::Fs, AsyncWorkPriority::NonCriticalReadList)
|
|
||||||
);
|
|
||||||
controller.stop().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn async_work_lane_has_one_active_job() {
|
|
||||||
let lane = Arc::new(AsyncWorkLane::default());
|
|
||||||
let mut controller =
|
|
||||||
AsyncWorkLaneController::start(AsyncWorkLaneConfig::default(), Arc::clone(&lane));
|
|
||||||
let (first_started_tx, first_started_rx) = mpsc::channel();
|
|
||||||
let (release_first_tx, release_first_rx) = mpsc::channel();
|
|
||||||
let (second_started_tx, second_started_rx) = mpsc::channel();
|
|
||||||
|
|
||||||
let first = lane
|
|
||||||
.submit(AsyncWorkJobKind::Test, move |_| {
|
|
||||||
first_started_tx.send(()).unwrap();
|
|
||||||
release_first_rx.recv().unwrap();
|
|
||||||
AsyncWorkJobOutcome::Completed
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
let second = lane
|
|
||||||
.submit(AsyncWorkJobKind::Test, move |_| {
|
|
||||||
second_started_tx.send(()).unwrap();
|
|
||||||
AsyncWorkJobOutcome::Completed
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
first_started_rx.recv().unwrap();
|
|
||||||
assert_eq!(lane.active_job().unwrap().id, first);
|
|
||||||
assert_eq!(lane.pending_depth(), 1);
|
|
||||||
assert!(second_started_rx.try_recv().is_err());
|
|
||||||
|
|
||||||
release_first_tx.send(()).unwrap();
|
|
||||||
second_started_rx.recv().unwrap();
|
|
||||||
assert_eq!(lane.telemetry().last_started_job_id, Some(second));
|
|
||||||
controller.stop().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn async_work_lane_supports_cooperative_cancellation() {
|
|
||||||
let lane = Arc::new(AsyncWorkLane::default());
|
|
||||||
let mut controller =
|
|
||||||
AsyncWorkLaneController::start(AsyncWorkLaneConfig::default(), Arc::clone(&lane));
|
|
||||||
let (started_tx, started_rx) = mpsc::channel();
|
|
||||||
|
|
||||||
let job_id = lane
|
|
||||||
.submit(AsyncWorkJobKind::Test, move |ctx| {
|
|
||||||
started_tx.send(()).unwrap();
|
|
||||||
while !ctx.is_canceled() {
|
|
||||||
std::thread::yield_now();
|
|
||||||
}
|
|
||||||
AsyncWorkJobOutcome::Canceled
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
started_rx.recv().unwrap();
|
|
||||||
assert!(lane.request_cancel(job_id));
|
|
||||||
controller.stop().unwrap();
|
|
||||||
assert_eq!(lane.telemetry().canceled_jobs, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn async_work_lane_rejects_submit_after_shutdown() {
|
|
||||||
let lane = Arc::new(AsyncWorkLane::default());
|
|
||||||
lane.request_shutdown();
|
|
||||||
|
|
||||||
let result = lane.submit(AsyncWorkJobKind::Test, |_| AsyncWorkJobOutcome::Completed);
|
|
||||||
assert_eq!(result, Err(AsyncWorkLaneError::Shutdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn async_work_lane_shutdown_discards_pending_jobs() {
|
|
||||||
let lane = Arc::new(AsyncWorkLane::default());
|
|
||||||
lane.submit(AsyncWorkJobKind::Test, |_| AsyncWorkJobOutcome::Completed).unwrap();
|
|
||||||
assert_eq!(lane.pending_depth(), 1);
|
|
||||||
|
|
||||||
lane.request_shutdown();
|
|
||||||
assert!(lane.wait_take().is_none());
|
|
||||||
assert_eq!(lane.pending_depth(), 0);
|
|
||||||
assert_eq!(lane.telemetry().shutdown_discards, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,4 +8,4 @@ pub use fs_backend::FsBackend;
|
|||||||
pub use fs_entry::FsEntry;
|
pub use fs_entry::FsEntry;
|
||||||
pub use fs_error::FsError;
|
pub use fs_error::FsError;
|
||||||
pub use fs_state::FsState;
|
pub use fs_state::FsState;
|
||||||
pub use virtual_fs::{FsAsyncLaneOperation, VirtualFS};
|
pub use virtual_fs::VirtualFS;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
use crate::fs::{FsBackend, FsEntry, FsError};
|
use crate::fs::{FsBackend, FsEntry, FsError};
|
||||||
use crate::services::async_work::{AsyncWorkJobKind, AsyncWorkPriority};
|
|
||||||
|
|
||||||
/// Virtual Filesystem (VFS) interface for Prometeu.
|
/// Virtual Filesystem (VFS) interface for Prometeu.
|
||||||
///
|
///
|
||||||
@ -14,29 +13,6 @@ pub struct VirtualFS {
|
|||||||
backend: Option<Box<dyn FsBackend>>,
|
backend: Option<Box<dyn FsBackend>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum FsAsyncLaneOperation {
|
|
||||||
Write,
|
|
||||||
Delete,
|
|
||||||
Config,
|
|
||||||
Read,
|
|
||||||
ListDir,
|
|
||||||
Exists,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FsAsyncLaneOperation {
|
|
||||||
pub const fn job_kind(self) -> AsyncWorkJobKind {
|
|
||||||
AsyncWorkJobKind::Fs
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn priority(self) -> AsyncWorkPriority {
|
|
||||||
match self {
|
|
||||||
Self::Write | Self::Delete | Self::Config => AsyncWorkPriority::FsWriteConfig,
|
|
||||||
Self::Read | Self::ListDir | Self::Exists => AsyncWorkPriority::NonCriticalReadList,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VirtualFS {
|
impl Default for VirtualFS {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
@ -266,18 +242,4 @@ mod tests {
|
|||||||
assert!(matches!(vfs.delete("/"), Err(FsError::PermissionDenied)));
|
assert!(matches!(vfs.delete("/"), Err(FsError::PermissionDenied)));
|
||||||
assert_eq!(calls.delete.load(Ordering::Relaxed), 0);
|
assert_eq!(calls.delete.load(Ordering::Relaxed), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fs_async_lane_operations_classify_priority_without_public_api_changes() {
|
|
||||||
assert_eq!(FsAsyncLaneOperation::Write.job_kind(), AsyncWorkJobKind::Fs);
|
|
||||||
assert_eq!(FsAsyncLaneOperation::Write.priority(), AsyncWorkPriority::FsWriteConfig);
|
|
||||||
assert_eq!(FsAsyncLaneOperation::Delete.priority(), AsyncWorkPriority::FsWriteConfig);
|
|
||||||
assert_eq!(FsAsyncLaneOperation::Config.priority(), AsyncWorkPriority::FsWriteConfig);
|
|
||||||
assert_eq!(FsAsyncLaneOperation::Read.priority(), AsyncWorkPriority::NonCriticalReadList);
|
|
||||||
assert_eq!(
|
|
||||||
FsAsyncLaneOperation::ListDir.priority(),
|
|
||||||
AsyncWorkPriority::NonCriticalReadList
|
|
||||||
);
|
|
||||||
assert_eq!(FsAsyncLaneOperation::Exists.priority(), AsyncWorkPriority::NonCriticalReadList);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
use crate::fs::{FsError, VirtualFS};
|
use crate::fs::{FsError, VirtualFS};
|
||||||
use crate::services::async_work::{AsyncWorkJobKind, AsyncWorkPriority};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub const MEMCARD_SLOT_COUNT: usize = 32;
|
pub const MEMCARD_SLOT_COUNT: usize = 32;
|
||||||
@ -51,30 +50,6 @@ pub struct MemcardWriteResult {
|
|||||||
pub bytes_written: u32,
|
pub bytes_written: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum MemcardAsyncLaneOperation {
|
|
||||||
SlotWrite,
|
|
||||||
SlotCommit,
|
|
||||||
SlotClear,
|
|
||||||
SlotRead,
|
|
||||||
SlotStat,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemcardAsyncLaneOperation {
|
|
||||||
pub const fn job_kind(self) -> AsyncWorkJobKind {
|
|
||||||
AsyncWorkJobKind::Memcard
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn priority(self) -> AsyncWorkPriority {
|
|
||||||
match self {
|
|
||||||
Self::SlotWrite | Self::SlotCommit | Self::SlotClear => {
|
|
||||||
AsyncWorkPriority::MemcardCommitWrite
|
|
||||||
}
|
|
||||||
Self::SlotRead | Self::SlotStat => AsyncWorkPriority::NonCriticalReadList,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct SlotImage {
|
struct SlotImage {
|
||||||
payload: Vec<u8>,
|
payload: Vec<u8>,
|
||||||
@ -453,29 +428,4 @@ mod tests {
|
|||||||
let stat = mem.slot_stat(&fs, 7, 2);
|
let stat = mem.slot_stat(&fs, 7, 2);
|
||||||
assert_eq!(stat.state, MemcardSlotState::Corrupt);
|
assert_eq!(stat.state, MemcardSlotState::Corrupt);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn memcard_async_lane_operations_classify_priority_without_changing_public_api() {
|
|
||||||
assert_eq!(MemcardAsyncLaneOperation::SlotWrite.job_kind(), AsyncWorkJobKind::Memcard);
|
|
||||||
assert_eq!(
|
|
||||||
MemcardAsyncLaneOperation::SlotWrite.priority(),
|
|
||||||
AsyncWorkPriority::MemcardCommitWrite
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
MemcardAsyncLaneOperation::SlotCommit.priority(),
|
|
||||||
AsyncWorkPriority::MemcardCommitWrite
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
MemcardAsyncLaneOperation::SlotClear.priority(),
|
|
||||||
AsyncWorkPriority::MemcardCommitWrite
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
MemcardAsyncLaneOperation::SlotRead.priority(),
|
|
||||||
AsyncWorkPriority::NonCriticalReadList
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
MemcardAsyncLaneOperation::SlotStat.priority(),
|
|
||||||
AsyncWorkPriority::NonCriticalReadList
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
pub mod async_work;
|
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
pub mod memcard;
|
pub mod memcard;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
|
|||||||
@ -757,64 +757,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
ret.push_int(status as i64);
|
ret.push_int(status as i64);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::AssetBacklogInfo => {
|
|
||||||
let info = platform.assets().backlog_info();
|
|
||||||
ret.push_int(info.status as i64);
|
|
||||||
ret.push_int(info.pending_count as i64);
|
|
||||||
ret.push_int(info.active_handle as i64);
|
|
||||||
ret.push_int(info.active_asset_id.unwrap_or(0) as i64);
|
|
||||||
ret.push_int(info.active_bank_type.map(|bank| bank as i64).unwrap_or(0));
|
|
||||||
ret.push_int(info.active_slot.map(|slot| slot as i64).unwrap_or(0));
|
|
||||||
ret.push_int(info.active_progress as i64);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Syscall::AssetBacklogPosition => {
|
|
||||||
let position = platform.assets().backlog_position(expect_int(args, 0)? as u32);
|
|
||||||
ret.push_int(position.status as i64);
|
|
||||||
ret.push_int(position.state as i64);
|
|
||||||
ret.push_int(position.position as i64);
|
|
||||||
ret.push_int(position.progress as i64);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Syscall::AssetBacklogMove => {
|
|
||||||
let new_position = expect_non_negative_usize(args, 1, "new_position")?;
|
|
||||||
let status =
|
|
||||||
platform.assets().backlog_move(expect_int(args, 0)? as u32, new_position);
|
|
||||||
ret.push_int(status as i64);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Syscall::AssetBacklogPromote => {
|
|
||||||
let status = platform.assets().backlog_promote(expect_int(args, 0)? as u32);
|
|
||||||
ret.push_int(status as i64);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Syscall::AssetBacklogDemote => {
|
|
||||||
let status = platform.assets().backlog_demote(expect_int(args, 0)? as u32);
|
|
||||||
ret.push_int(status as i64);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Syscall::AssetTargetStatus => {
|
|
||||||
let bank_type = match expect_int(args, 0)? {
|
|
||||||
0 => BankType::GLYPH,
|
|
||||||
1 => BankType::SOUNDS,
|
|
||||||
2 => BankType::SCENE,
|
|
||||||
other => {
|
|
||||||
return Err(VmFault::Trap(
|
|
||||||
TRAP_TYPE,
|
|
||||||
format!("Invalid asset bank type: {}", other),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let slot = expect_non_negative_usize(args, 1, "slot")?;
|
|
||||||
let target = platform.assets().target_status(bank_type, slot);
|
|
||||||
ret.push_int(target.status as i64);
|
|
||||||
ret.push_int(target.asset_id.unwrap_or(0) as i64);
|
|
||||||
ret.push_int(target.handle as i64);
|
|
||||||
ret.push_int(target.state as i64);
|
|
||||||
ret.push_int(target.position as i64);
|
|
||||||
ret.push_int(target.progress as i64);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Syscall::BankInfo => {
|
Syscall::BankInfo => {
|
||||||
let asset_type = match expect_int(args, 0)? as u32 {
|
let asset_type = match expect_int(args, 0)? as u32 {
|
||||||
0 => BankType::GLYPH,
|
0 => BankType::GLYPH,
|
||||||
|
|||||||
@ -180,102 +180,6 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() {
|
|||||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]);
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tick_asset_target_status_empty_slot_returns_status_first_payload() {
|
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
|
||||||
let mut log_service = LogService::new(4096);
|
|
||||||
let mut fs = VirtualFS::new();
|
|
||||||
let mut fs_state = FsState::Unmounted;
|
|
||||||
let mut memcard = MemcardService::new();
|
|
||||||
let mut open_files: HashMap<u32, String> = HashMap::new();
|
|
||||||
let mut next_handle = 1;
|
|
||||||
let mut vm = VirtualMachine::default();
|
|
||||||
let mut platform = TestPlatform::new();
|
|
||||||
let signals = InputSignals::default();
|
|
||||||
let code = assemble("PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
|
||||||
let program = serialized_single_function_module(
|
|
||||||
code,
|
|
||||||
vec![SyscallDecl {
|
|
||||||
module: "asset".into(),
|
|
||||||
name: "target_status".into(),
|
|
||||||
version: 1,
|
|
||||||
arg_slots: 2,
|
|
||||||
ret_slots: 6,
|
|
||||||
}],
|
|
||||||
);
|
|
||||||
let cartridge = cartridge_with_program(program, caps::ASSET);
|
|
||||||
|
|
||||||
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
|
|
||||||
let report = runtime.tick(
|
|
||||||
&mut log_service,
|
|
||||||
&mut fs,
|
|
||||||
&mut fs_state,
|
|
||||||
&mut memcard,
|
|
||||||
&mut open_files,
|
|
||||||
&mut next_handle,
|
|
||||||
&mut vm,
|
|
||||||
&signals,
|
|
||||||
&mut platform,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(report.is_none(), "target_status must not crash for empty slots");
|
|
||||||
assert!(vm.is_halted());
|
|
||||||
assert_eq!(
|
|
||||||
vm.operand_stack_top(6),
|
|
||||||
vec![
|
|
||||||
Value::Int64(0),
|
|
||||||
Value::Int64(0),
|
|
||||||
Value::Int64(LoadStatus::EMPTY as i64),
|
|
||||||
Value::Int64(1),
|
|
||||||
Value::Int64(0),
|
|
||||||
Value::Int64(AssetOpStatus::Ok as i64),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tick_asset_backlog_promote_unknown_handle_returns_status_not_crash() {
|
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
|
||||||
let mut log_service = LogService::new(4096);
|
|
||||||
let mut fs = VirtualFS::new();
|
|
||||||
let mut fs_state = FsState::Unmounted;
|
|
||||||
let mut memcard = MemcardService::new();
|
|
||||||
let mut open_files: HashMap<u32, String> = HashMap::new();
|
|
||||||
let mut next_handle = 1;
|
|
||||||
let mut vm = VirtualMachine::default();
|
|
||||||
let mut platform = TestPlatform::new();
|
|
||||||
let signals = InputSignals::default();
|
|
||||||
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
|
|
||||||
let program = serialized_single_function_module(
|
|
||||||
code,
|
|
||||||
vec![SyscallDecl {
|
|
||||||
module: "asset".into(),
|
|
||||||
name: "backlog_promote".into(),
|
|
||||||
version: 1,
|
|
||||||
arg_slots: 1,
|
|
||||||
ret_slots: 1,
|
|
||||||
}],
|
|
||||||
);
|
|
||||||
let cartridge = cartridge_with_program(program, caps::ASSET);
|
|
||||||
|
|
||||||
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
|
|
||||||
let report = runtime.tick(
|
|
||||||
&mut log_service,
|
|
||||||
&mut fs,
|
|
||||||
&mut fs_state,
|
|
||||||
&mut memcard,
|
|
||||||
&mut open_files,
|
|
||||||
&mut next_handle,
|
|
||||||
&mut vm,
|
|
||||||
&signals,
|
|
||||||
&mut platform,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(report.is_none(), "unknown backlog handle must not crash");
|
|
||||||
assert!(vm.is_halted());
|
|
||||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_bank_info_returns_slot_summary_not_json() {
|
fn tick_bank_info_returns_slot_summary_not_json() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":35,"PLN":129,"LSN":51,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":34,"PLN":123,"LSN":50,"CLSN":1}}
|
||||||
{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}
|
{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}
|
||||||
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]}
|
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]}
|
||||||
{"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
@ -18,9 +18,9 @@
|
|||||||
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-06-04","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
{"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-06-04","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
{"type":"discussion","id":"DSC-0009","status":"done","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-07-01","tags":["perf","asset","fs","async","scheduler","runtime"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0050","file":"discussion/lessons/DSC-0009-perf-async-background-work-lanes-for-assets-and-fs/LSN-0050-serial-async-lanes-bound-backlog-complexity.md","status":"done","created_at":"2026-07-01","updated_at":"2026-07-01"}]}
|
{"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0010","status":"done","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"discussion/lessons/DSC-0010-perf-host-desktop-frame-pacing-and-presentation/LSN-0036-frame-publication-and-host-invalidation-must-be-separate.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]}
|
{"type":"discussion","id":"DSC-0010","status":"done","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"discussion/lessons/DSC-0010-perf-host-desktop-frame-pacing-and-presentation/LSN-0036-frame-publication-and-host-invalidation-must-be-separate.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]}
|
||||||
{"type":"discussion","id":"DSC-0011","status":"abandoned","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-06-28","tags":[],"agendas":[{"id":"AGD-0010","file":"AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"abandoned","created_at":"2026-03-27","updated_at":"2026-06-28","_override_reason":"User explicitly chose to remove this agenda because it no longer applies to the current render scenario; future optimizations can be discussed when needed."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to abandon this discussion because it no longer applies to the current render scenario; future optimizations can be discussed when needed."}
|
{"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0012","status":"done","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-04-19","tags":["perf","runtime","syscall","telemetry","debug","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0034","file":"discussion/lessons/DSC-0012-perf-runtime-introspection-syscalls/LSN-0034-host-owned-debug-boundaries.md","status":"done","created_at":"2026-04-19","updated_at":"2026-04-19"}]}
|
{"type":"discussion","id":"DSC-0012","status":"done","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-04-19","tags":["perf","runtime","syscall","telemetry","debug","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0034","file":"discussion/lessons/DSC-0012-perf-runtime-introspection-syscalls/LSN-0034-host-owned-debug-boundaries.md","status":"done","created_at":"2026-04-19","updated_at":"2026-04-19"}]}
|
||||||
{"type":"discussion","id":"DSC-0013","status":"done","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0013-perf-host-debug-overlay-isolation/LSN-0027-host-debug-overlay-isolation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
{"type":"discussion","id":"DSC-0013","status":"done","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0013-perf-host-debug-overlay-isolation/LSN-0027-host-debug-overlay-isolation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
{"type":"discussion","id":"DSC-0024","status":"done","ticket":"generic-memory-bank-slot-contract","title":"Agenda - Generic Memory Bank Slot Contract","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["runtime","asset","memory-bank","slots","host"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"discussion/lessons/DSC-0024-generic-memory-bank-slot-contract/LSN-0029-slot-first-bank-telemetry-belongs-in-asset-manager.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
{"type":"discussion","id":"DSC-0024","status":"done","ticket":"generic-memory-bank-slot-contract","title":"Agenda - Generic Memory Bank Slot Contract","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["runtime","asset","memory-bank","slots","host"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"discussion/lessons/DSC-0024-generic-memory-bank-slot-contract/LSN-0029-slot-first-bank-telemetry-belongs-in-asset-manager.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
|
|||||||
@ -1,100 +0,0 @@
|
|||||||
---
|
|
||||||
id: LSN-0050
|
|
||||||
ticket: perf-async-background-work-lanes-for-assets-and-fs
|
|
||||||
title: Serial Async Lanes Bound Backlog Complexity
|
|
||||||
created: 2026-07-01
|
|
||||||
tags: [runtime, asset, async, scheduler, telemetry]
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The async asset work moved from request-owned worker creation toward a
|
|
||||||
runtime-owned serial lane. The important shift is not just "use a worker
|
|
||||||
thread"; it is the introduction of a third logical execution lane with explicit
|
|
||||||
ownership, priority, cancellation, progress, and telemetry.
|
|
||||||
|
|
||||||
This work followed `DEC-0034` and plans `PLN-0123` through `PLN-0128`. The
|
|
||||||
published model separates:
|
|
||||||
|
|
||||||
- main runtime execution, where guest-visible state is committed;
|
|
||||||
- render worker execution, where closed render packets are consumed;
|
|
||||||
- async IO/decode/persistence work, where one background job is active at a
|
|
||||||
time.
|
|
||||||
|
|
||||||
## Key Decisions
|
|
||||||
|
|
||||||
### Async Work Lane and Asset Backlog Contract
|
|
||||||
|
|
||||||
**What:** Asset IO/decode and compatible persistence work use a runtime-owned
|
|
||||||
serial async lane. Asset requests are queued by target bank slot, not by
|
|
||||||
unbounded transient request identity.
|
|
||||||
|
|
||||||
**Why:** A serial lane keeps the runtime observable and portable. It preserves
|
|
||||||
the intended hardware mental model while avoiding a desktop-biased thread pool
|
|
||||||
or one OS thread per asset request.
|
|
||||||
|
|
||||||
**Trade-offs:** The lane intentionally sacrifices parallel decode throughput in
|
|
||||||
exchange for bounded state, deterministic ownership, simpler telemetry, and
|
|
||||||
clear priority rules. If future implementations add more physical parallelism,
|
|
||||||
they still need to preserve the logical serial contract where the spec requires
|
|
||||||
it.
|
|
||||||
|
|
||||||
## Patterns and Algorithms
|
|
||||||
|
|
||||||
The asset backlog is bounded by target identity. A request targets a concrete
|
|
||||||
bank type and slot. A newer request for the same target supersedes the older
|
|
||||||
pending request, and an older active result is discarded when its generation is
|
|
||||||
no longer current.
|
|
||||||
|
|
||||||
Stable handles observe slots. A handle should not be treated as a worker job or
|
|
||||||
thread token. The durable identity is the bank slot plus request generation:
|
|
||||||
slot state says what is resident; request state says what is queued, active,
|
|
||||||
ready, canceled, superseded, or failed.
|
|
||||||
|
|
||||||
Commit remains a main-lane operation. The background lane may read and decode,
|
|
||||||
but publication into resident runtime state happens at predictable ownership
|
|
||||||
points. This keeps VM execution, render handoff, and frame observation from
|
|
||||||
racing with background mutation.
|
|
||||||
|
|
||||||
Progress and telemetry are closure-oriented. Use integer progress and update
|
|
||||||
expensive aggregate telemetry when jobs close, not inside decode loops. Tests
|
|
||||||
should synchronize on explicit state transitions rather than sleeps.
|
|
||||||
|
|
||||||
Priority is part of the lane contract. Memcard commit/write work can outrank
|
|
||||||
ordinary asset loads, FS write/config work can be represented internally, and
|
|
||||||
non-critical read/list work belongs below asset loads. Public FS semantics
|
|
||||||
remain owned by the filesystem discussion; sharing the lane is not permission
|
|
||||||
to decide the FS API here.
|
|
||||||
|
|
||||||
## Pitfalls
|
|
||||||
|
|
||||||
Do not hide concurrency behind per-request `thread::spawn`. That appears simple
|
|
||||||
locally but loses boundedness, priority, cancellation, and telemetry.
|
|
||||||
|
|
||||||
Do not let a handle mean "the current background job." Handles must survive
|
|
||||||
empty slots, completed work, cancellation, superseding, errors, and already
|
|
||||||
resident fast paths.
|
|
||||||
|
|
||||||
Do not install background results directly from the worker. Decode completion is
|
|
||||||
not the same as publication. Use request generation checks before committing a
|
|
||||||
result.
|
|
||||||
|
|
||||||
Do not turn operational states into traps. Queued, active, canceled,
|
|
||||||
superseded, error, and backend-unavailable outcomes belong in status-first
|
|
||||||
surfaces unless the caller violated the structural ABI.
|
|
||||||
|
|
||||||
Do not reopen FS public API scope while wiring internal async-lane consumers.
|
|
||||||
The lane can support FS-style work without deciding request/poll semantics.
|
|
||||||
|
|
||||||
## Takeaways
|
|
||||||
|
|
||||||
- A runtime async lane is an ownership boundary, not just an implementation
|
|
||||||
thread.
|
|
||||||
- Backlog keying by target slot bounds complexity better than queue length
|
|
||||||
limits exposed to the guest.
|
|
||||||
- Stable slot handles plus request generations prevent stale async results from
|
|
||||||
mutating newer state.
|
|
||||||
- Main-lane commit keeps background IO/decode compatible with deterministic
|
|
||||||
runtime publication.
|
|
||||||
- Telemetry belongs at state transitions and job closure, not inside hot decode
|
|
||||||
loops.
|
|
||||||
@ -22,19 +22,6 @@ Sem um contrato claro de `home` por app, a API tende a crescer com semantica inc
|
|||||||
1. Todo `app` acessa somente sua `home` logica.
|
1. Todo `app` acessa somente sua `home` logica.
|
||||||
2. Nunca ha acesso direto ao filesystem global do host pela userland.
|
2. Nunca ha acesso direto ao filesystem global do host pela userland.
|
||||||
3. O runtime `fs` interno continua cobrindo tanto `game` quanto `app`.
|
3. O runtime `fs` interno continua cobrindo tanto `game` quanto `app`.
|
||||||
4. Existe uma async IO lane compartilhavel por assets, memcard e FS. Esta
|
|
||||||
agenda deve decidir a API publica de FS considerando essa lane, mas a
|
|
||||||
existencia da lane ja esta fechada por `DEC-0034`.
|
|
||||||
|
|
||||||
## Fronteira com a Async IO Lane
|
|
||||||
|
|
||||||
`DEC-0034` fecha a existencia de uma lane serial para trabalho async de IO. FS
|
|
||||||
pode consumir essa lane internamente para escrita, configuracao, leitura ou
|
|
||||||
listagem, conforme prioridade operacional definida pelo runtime.
|
|
||||||
|
|
||||||
Esta agenda continua dona da decisao sobre API publica de FS para `app home`.
|
|
||||||
Portanto, a existencia da lane nao implica criar agora request handles, polling
|
|
||||||
publico ou novas syscalls de FS. Esses shapes permanecem em aberto aqui.
|
|
||||||
|
|
||||||
## Alvo da Discussao
|
## Alvo da Discussao
|
||||||
|
|
||||||
@ -129,8 +116,6 @@ No perfil `app` (`home` sandbox), esta agenda passa a ser a fonte normativa para
|
|||||||
2. `rename` entra no MVP ou pode ficar para fase seguinte?
|
2. `rename` entra no MVP ou pode ficar para fase seguinte?
|
||||||
3. Qual conjunto minimo de metadados garante portabilidade real entre hosts?
|
3. Qual conjunto minimo de metadados garante portabilidade real entre hosts?
|
||||||
4. Qual grau de atomicidade e obrigatorio para escrita de arquivo no v1?
|
4. Qual grau de atomicidade e obrigatorio para escrita de arquivo no v1?
|
||||||
5. Quais operacoes de FS devem consumir a async IO lane e quais permanecem
|
|
||||||
sincrono-aparentes para a userland?
|
|
||||||
|
|
||||||
## Dependencias
|
## Dependencias
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
id: AGD-0008
|
||||||
|
ticket: perf-async-background-work-lanes-for-assets-and-fs
|
||||||
|
title: Agenda - [PERF] Async Background Work Lanes for Assets and FS
|
||||||
|
status: open
|
||||||
|
created: 2026-03-27
|
||||||
|
resolved:
|
||||||
|
decision:
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agenda - [PERF] Async Background Work Lanes for Assets and FS
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
`asset.load()` hoje cria uma thread do SO por requisicao. Ao mesmo tempo, `fs` ainda nao tem uma politica clara para sync assincrono barato em hardware simples.
|
||||||
|
|
||||||
|
O projeto precisa de paralelismo para IO/decode/sync, mas o target inclui handheld DIY e hardware barato, onde um pool grande ou explosao de threads pode piorar a latencia em vez de melhorar.
|
||||||
|
|
||||||
|
## Dor
|
||||||
|
|
||||||
|
- `thread::spawn` por request escala mal e cria jitter.
|
||||||
|
- assets e `fs` competem por IO sem uma politica unica de fila/prioridade.
|
||||||
|
- sem lane dedicada, operacoes de background tendem a vazar custo para o main loop.
|
||||||
|
- sem disciplina, o host desktop vira referencia errada para hardware fraco.
|
||||||
|
|
||||||
|
## Hotspots Atuais
|
||||||
|
|
||||||
|
- [asset.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/asset.rs#L353)
|
||||||
|
- [tick.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs#L53)
|
||||||
|
- [runner.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/runner.rs#L315)
|
||||||
|
|
||||||
|
## Alvo da Discussao
|
||||||
|
|
||||||
|
Fechar um modelo de execucao assincrona para `asset` e `fs` que seja previsivel em hardware simples.
|
||||||
|
|
||||||
|
## O Que Precisa Ser Definido
|
||||||
|
|
||||||
|
1. Topologia de workers.
|
||||||
|
Escolher entre:
|
||||||
|
- uma thread dedicada para `asset` e outra para `fs`;
|
||||||
|
- um worker unico multiplexando filas;
|
||||||
|
- um pool minimo e fixo;
|
||||||
|
- proibicao explicita de `spawn` por request.
|
||||||
|
|
||||||
|
2. Separacao por dominio.
|
||||||
|
Decidir se `asset` e `fs` compartilham scheduler/fila ou se cada dominio tem lane propria.
|
||||||
|
|
||||||
|
3. Politica de prioridade.
|
||||||
|
Definir:
|
||||||
|
- prioridade de loads visuais vs audio;
|
||||||
|
- prioridade de sync de save/config;
|
||||||
|
- limite de trabalho por frame para install/commit.
|
||||||
|
|
||||||
|
4. Modelo de retorno.
|
||||||
|
Fechar como o guest observa backlog, cancelamento e saturacao:
|
||||||
|
- status imediato de fila cheia;
|
||||||
|
- status de pending;
|
||||||
|
- metrica de backlog;
|
||||||
|
- politica de cancelamento.
|
||||||
|
|
||||||
|
5. Orçamento para hardware barato.
|
||||||
|
Definir quantas threads o runtime pode assumir como baseline.
|
||||||
|
|
||||||
|
## Open Questions de Arquitetura
|
||||||
|
|
||||||
|
1. O v1 precisa de lanes separadas para `asset` e `fs` ou basta uma fila central com classes de prioridade?
|
||||||
|
2. Decode de asset fica no mesmo worker do IO ou em fase distinta?
|
||||||
|
3. Commit/install no device continua no main thread por determinismo?
|
||||||
|
|
||||||
|
## Dependencias
|
||||||
|
|
||||||
|
- `../specs/09-events-and-concurrency.md`
|
||||||
|
- `../specs/15-asset-management.md`
|
||||||
|
- `../specs/16a-syscall-policies.md`
|
||||||
|
- `014-app-home-filesystem-surface-and-semantics.md`
|
||||||
|
|
||||||
|
## Criterio de Saida Desta Agenda
|
||||||
|
|
||||||
|
Pode virar PR quando houver decisao escrita sobre:
|
||||||
|
|
||||||
|
- numero e tipo de workers aceitos no baseline;
|
||||||
|
- fila/prioridade de `asset` e `fs`;
|
||||||
|
- proibicao ou aceitacao limitada de `thread::spawn` por request;
|
||||||
|
- modelo de status/telemetria para backlog e saturacao.
|
||||||
@ -0,0 +1,409 @@
|
|||||||
|
---
|
||||||
|
id: AGD-0010
|
||||||
|
ticket: perf-gfx-render-pipeline-and-dirty-regions
|
||||||
|
title: Agenda - [PERF] GFX Render Pipeline and Dirty Regions
|
||||||
|
status: open
|
||||||
|
created: 2026-03-27
|
||||||
|
resolved:
|
||||||
|
decision:
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agenda - [PERF] GFX Render Pipeline and Dirty Regions
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
O renderer `gfx` recompõe a cena inteira a cada frame logico, mesmo quando a mudanca visual e pequena.
|
||||||
|
|
||||||
|
Hoje `render_all()` reconstrói buckets de sprites, escaneia os 512 sprites, redesenha layers e aplica dois passes fullscreen de fade sem politica de invalidacao.
|
||||||
|
|
||||||
|
Ao mesmo tempo, a arquitetura atual ja opera como um framebuffer destrutivo em memoria: draws escrevem diretamente no buffer de trabalho e operacoes posteriores sobrescrevem o que estiver por baixo. A discussao nao e migrar para um modelo tipo GPU, scene graph ou retained rendering.
|
||||||
|
|
||||||
|
Existe ainda um driver de produto importante: o objetivo continua sendo viabilizar hardware handheld proprio com limitacoes reais de orcamento e com economia de memoria o mais agressiva possivel. Isso significa que ganhos de CPU nao podem ser avaliados isoladamente; custo de RAM adicional, buffers extras e estruturas auxiliares entram diretamente no criterio de aceitacao.
|
||||||
|
|
||||||
|
## Dor
|
||||||
|
|
||||||
|
- custo visual basico cresce demais para hardware simples.
|
||||||
|
- pequenos updates pagam preco de full redraw.
|
||||||
|
- fade, HUD e world composition ficam sempre no caminho critico.
|
||||||
|
|
||||||
|
## Hotspots Atuais
|
||||||
|
|
||||||
|
- [gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs#L563)
|
||||||
|
- [gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs#L671)
|
||||||
|
|
||||||
|
## Alvo da Discussao
|
||||||
|
|
||||||
|
Definir ate onde o v1 pode sofisticar o modelo de desenho destrutivo em memoria para reduzir custo e manter previsibilidade, sem migrar para um renderer de estilo GPU.
|
||||||
|
|
||||||
|
## O Que Precisa Ser Definido
|
||||||
|
|
||||||
|
1. Perfil real de custo.
|
||||||
|
Medir separadamente:
|
||||||
|
- rebuild de buckets;
|
||||||
|
- rasterizacao de layers;
|
||||||
|
- rasterizacao de sprites;
|
||||||
|
- fades fullscreen;
|
||||||
|
- conversao/copia no host;
|
||||||
|
- apresentacao no host.
|
||||||
|
|
||||||
|
2. Granularidade minima de invalidacao compativel com framebuffer destrutivo.
|
||||||
|
Escolher entre:
|
||||||
|
- dirty flag por frame inteiro;
|
||||||
|
- dirty flags por subsistema (`world`, `hud`, `fade`, `sprites`);
|
||||||
|
- dirty regions restritas e derivadas de primitivas/sprites;
|
||||||
|
- combinacao minima viavel.
|
||||||
|
|
||||||
|
3. Buckets e traversal de sprites.
|
||||||
|
Decidir se os buckets continuam sendo reconstruidos por frame ou se viram estrutura incremental.
|
||||||
|
|
||||||
|
4. Fades.
|
||||||
|
Definir se fade neutro precisa bypass total e se fade parcial pode operar por regiao/camada.
|
||||||
|
|
||||||
|
5. Cache de composicao.
|
||||||
|
Delimitar se layers estaticas/HUD podem manter cache intermediario.
|
||||||
|
|
||||||
|
6. Primitivas e contrato operacional.
|
||||||
|
Definir quais draws devem continuar sendo vistos como escrita destrutiva direta no framebuffer e onde valem aceleracoes internas sem mudar a semantica observavel.
|
||||||
|
|
||||||
|
7. Meta de custo.
|
||||||
|
Fechar qual teto de pixels/trabalho por frame e aceitavel no baseline.
|
||||||
|
|
||||||
|
8. Orcamento de memoria.
|
||||||
|
Definir quais otimizacoes aceitam memoria adicional e quais precisam caber no modelo mais economico possivel para handheld barato.
|
||||||
|
|
||||||
|
## Open Questions de Arquitetura
|
||||||
|
|
||||||
|
1. Quais partes do custo atual estao no `render_all()` e quais partes estao na conversao/apresentacao do host?
|
||||||
|
2. Qual o maior ganho de curto prazo dentro do modelo atual: buckets, fades, HUD, primitivas ou host copy/convert?
|
||||||
|
3. Dirty regions restritas quebram alguma expectativa de determinismo visivel ou sao apenas um detalhe interno de implementacao?
|
||||||
|
4. O HUD deve continuar no mesmo pipeline da world scene?
|
||||||
|
5. Quais primitivas merecem tratamento especial para reduzir overdraw sem mudar a semantica de sobrescrita?
|
||||||
|
6. Qual o teto aceitavel de memoria extra para caches, buffers separados ou estruturas auxiliares no handheld alvo?
|
||||||
|
|
||||||
|
## Problemas de Medio e Grande Porte Identificados
|
||||||
|
|
||||||
|
### 1. Composicao completa do framebuffer a cada frame logico
|
||||||
|
|
||||||
|
`render_all()` recompõe o `back` inteiro por software, mesmo quando a mudanca visual e pequena.
|
||||||
|
|
||||||
|
Impacto:
|
||||||
|
|
||||||
|
- custo cresce com a quantidade de pixels tocados, nao apenas com a quantidade de estado alterado;
|
||||||
|
- layers, sprites e fades disputam o mesmo budget de CPU;
|
||||||
|
- o modelo atual favorece previsibilidade sem ainda ter atalhos internos suficientes.
|
||||||
|
|
||||||
|
### 2. Rasterizacao pixel a pixel de tilemaps
|
||||||
|
|
||||||
|
Cada layer visivel percorre tiles e, para cada tile nao vazio, resolve indices e escreve pixel a pixel no framebuffer.
|
||||||
|
|
||||||
|
Impacto:
|
||||||
|
|
||||||
|
- o custo real esta mais na escrita de pixels do que na manutencao do `TileMap`;
|
||||||
|
- scroll e barato como estado, mas caro na hora de compor a imagem se tudo for repintado;
|
||||||
|
- layers estaticas continuam pagando custo de rasterizacao em toda recomposicao.
|
||||||
|
|
||||||
|
### 3. Rasterizacao pixel a pixel de sprites e escolha por bucket full scan
|
||||||
|
|
||||||
|
Os 512 sprites sao varridos para reconstruir buckets e os sprites ativos sao desenhados pixel a pixel.
|
||||||
|
|
||||||
|
Impacto:
|
||||||
|
|
||||||
|
- quando poucos sprites mudam, ainda existe custo fixo de rebuild dos buckets;
|
||||||
|
- overdraw entre sprites e layers pode explodir o numero de writes no `back`;
|
||||||
|
- a escolha de sprites ativos/prioridade ainda e simples demais para cenarios com alta ocupacao.
|
||||||
|
|
||||||
|
### 4. Fades fullscreen com custo proporcional ao frame inteiro
|
||||||
|
|
||||||
|
`scene fade` e `hud fade` percorrem o framebuffer inteiro quando ativos.
|
||||||
|
|
||||||
|
Impacto:
|
||||||
|
|
||||||
|
- custo grande e previsivel, mas potencialmente desproporcional;
|
||||||
|
- fade neutro ja pode ser bypass, mas fade parcial ainda custa uma passada completa;
|
||||||
|
- dois passes fullscreen no mesmo frame podem virar gargalo antes de layers ou sprites.
|
||||||
|
|
||||||
|
### 5. HUD no mesmo caminho critico da composicao principal
|
||||||
|
|
||||||
|
O HUD e redesenhado como tilemap depois da world scene e antes do `hud fade`.
|
||||||
|
|
||||||
|
Impacto:
|
||||||
|
|
||||||
|
- HUD estatico continua gerando trabalho por frame;
|
||||||
|
- mistura responsabilidades de world/UI no mesmo budget de composicao;
|
||||||
|
- dificulta isolar ganho de cache ou dirtying so para interface.
|
||||||
|
|
||||||
|
### 6. Conversao obrigatoria de RGB565 para RGBA8 no host
|
||||||
|
|
||||||
|
No `RedrawRequested`, o host percorre todo o `front_buffer` e converte para o frame do `pixels`.
|
||||||
|
|
||||||
|
Impacto:
|
||||||
|
|
||||||
|
- ha custo de leitura do framebuffer inteiro e escrita de um segundo buffer inteiro;
|
||||||
|
- esse custo existe mesmo quando o host esta apenas apresentando o mesmo quadro logico;
|
||||||
|
- pode competir com o custo do renderer sem estar visivel na discussao do `gfx`.
|
||||||
|
|
||||||
|
### 7. Pipeline serializado no host desktop
|
||||||
|
|
||||||
|
`tick`, `render_all()`, conversao de formato e apresentacao ocorrem no mesmo fluxo operacional.
|
||||||
|
|
||||||
|
Impacto:
|
||||||
|
|
||||||
|
- o frame time final acumula custo de runtime, composicao, copy/convert e present;
|
||||||
|
- falta desacoplamento para esconder latencia entre producao e apresentacao;
|
||||||
|
- dificulta atribuir gargalo sem instrumentacao por etapa.
|
||||||
|
|
||||||
|
## Estudo Inicial de Possiveis Optimizacoes
|
||||||
|
|
||||||
|
### A. Dirty flags por subsistema em vez de dirty rect generico
|
||||||
|
|
||||||
|
Ideia:
|
||||||
|
|
||||||
|
- flags independentes para `world`, `sprites`, `hud`, `scene_fade`, `hud_fade`, `host_present`.
|
||||||
|
|
||||||
|
Vantagens:
|
||||||
|
|
||||||
|
- preserva a semantica de framebuffer destrutivo;
|
||||||
|
- reduz recomposicao desnecessaria quando so um subsistema mudou;
|
||||||
|
- e muito mais simples de validar do que dirty regions arbitrarias.
|
||||||
|
|
||||||
|
Riscos:
|
||||||
|
|
||||||
|
- exige definir dependencias claras entre world, sprites, HUD e fades;
|
||||||
|
- pode induzir falsos positivos conservadores, o que e aceitavel no v1.
|
||||||
|
|
||||||
|
### B. Rebuild incremental ou condicional dos buckets de sprite
|
||||||
|
|
||||||
|
Ideia:
|
||||||
|
|
||||||
|
- so reconstruir buckets quando algum `GfxSetSprite` mudar atividade, prioridade, banco ou tile;
|
||||||
|
- opcionalmente manter contadores/sinais de mutacao da OAM.
|
||||||
|
|
||||||
|
Vantagens:
|
||||||
|
|
||||||
|
- elimina custo fixo por frame quando OAM permanece estavel;
|
||||||
|
- combina com a ideia de sprites como estado, nao como draw list efemera.
|
||||||
|
|
||||||
|
Riscos:
|
||||||
|
|
||||||
|
- precisa invalidacao correta para evitar bucket desatualizado;
|
||||||
|
- ganho pode ser pequeno se o custo dominante estiver no draw dos sprites, nao no rebuild.
|
||||||
|
|
||||||
|
### C. Separar world e HUD em buffers logicos distintos
|
||||||
|
|
||||||
|
Ideia:
|
||||||
|
|
||||||
|
- manter um buffer da world scene e um buffer do HUD, compondo no final apenas quando necessario.
|
||||||
|
|
||||||
|
Vantagens:
|
||||||
|
|
||||||
|
- HUD estatico deixa de participar da recomposicao da world;
|
||||||
|
- permite cache e fade especificos para cada dominio;
|
||||||
|
- se alinha bem com a separacao conceitual entre cena e interface.
|
||||||
|
|
||||||
|
Riscos:
|
||||||
|
|
||||||
|
- aumenta uso de memoria e pontos de sincronizacao;
|
||||||
|
- precisa manter semantica clara de sobrescrita entre mundo e HUD.
|
||||||
|
- pode ser inviavel no perfil de handheld barato se exigir buffers adicionais permanentes.
|
||||||
|
|
||||||
|
### D. Cache de layer estatica ou composicao parcial da world
|
||||||
|
|
||||||
|
Ideia:
|
||||||
|
|
||||||
|
- layers que nao mudam podem manter imagem intermediaria pronta;
|
||||||
|
- scroll, tile updates ou troca de bank invalidam o cache correspondente.
|
||||||
|
|
||||||
|
Vantagens:
|
||||||
|
|
||||||
|
- reduz rasterizacao repetida de cenario estavel;
|
||||||
|
- aproxima o ganho de hardware tile-based sem abandonar software raster.
|
||||||
|
|
||||||
|
Riscos:
|
||||||
|
|
||||||
|
- cache amplo demais vira complexidade estrutural;
|
||||||
|
- se scroll muda constantemente, o ganho cai bastante.
|
||||||
|
- pode consumir memoria demais para um hardware com orcamento agressivo.
|
||||||
|
|
||||||
|
### E. Otimizacao de fades
|
||||||
|
|
||||||
|
Ideia:
|
||||||
|
|
||||||
|
- bypass total para fade neutro;
|
||||||
|
- opcionalmente aplicar fade apenas sobre buffers relevantes;
|
||||||
|
- estudar LUTs ou blend mais barato por pixel.
|
||||||
|
|
||||||
|
Vantagens:
|
||||||
|
|
||||||
|
- ataca um custo fullscreen claramente delimitado;
|
||||||
|
- baixo risco conceitual.
|
||||||
|
|
||||||
|
Riscos:
|
||||||
|
|
||||||
|
- fade parcial por regiao pode complicar demais o contrato;
|
||||||
|
- LUT ajuda aritmetica, mas nao elimina custo de varrer memoria.
|
||||||
|
|
||||||
|
### F. Melhorias em primitivas e spans de memoria
|
||||||
|
|
||||||
|
Ideia:
|
||||||
|
|
||||||
|
- acelerar `fill_rect`, linhas horizontais/verticais, clears e possiveis blits contiguos;
|
||||||
|
- explorar caminhos de row spans contiguos em vez de loops mais genericos.
|
||||||
|
|
||||||
|
Vantagens:
|
||||||
|
|
||||||
|
- melhora direta do modelo de desenho destrutivo;
|
||||||
|
- baixo risco arquitetural;
|
||||||
|
- cria fundacao para outros atalhos internos.
|
||||||
|
|
||||||
|
Riscos:
|
||||||
|
|
||||||
|
- ganho localizado se o workload dominante vier de tile/sprite compositing;
|
||||||
|
- precisa medir por primitive class.
|
||||||
|
|
||||||
|
### G. Reduzir custo de copy/convert no host
|
||||||
|
|
||||||
|
Ideia:
|
||||||
|
|
||||||
|
- medir separadamente `draw_rgb565_to_rgba8` e `pixels.render()`;
|
||||||
|
- estudar formato mais proximo do host ou conversao mais barata;
|
||||||
|
- evitar redraw host quando nao houver novo front relevante.
|
||||||
|
|
||||||
|
Vantagens:
|
||||||
|
|
||||||
|
- ataca custo fora do `gfx` que ainda entra no frame time total;
|
||||||
|
- pode render ganho imediato no desktop host.
|
||||||
|
|
||||||
|
Riscos:
|
||||||
|
|
||||||
|
- parte do custo depende da stack `pixels/wgpu`, nao apenas do runtime;
|
||||||
|
- alguns ganhos podem ser especificos do host desktop e nao do contrato do console.
|
||||||
|
|
||||||
|
## Restricao de Plataforma
|
||||||
|
|
||||||
|
Qualquer recomendacao desta agenda deve ser filtrada por um criterio adicional:
|
||||||
|
|
||||||
|
- priorizar solucoes que melhorem custo sem multiplicar buffers;
|
||||||
|
- tratar memoria extra como recurso escasso de primeira classe;
|
||||||
|
- preferir flags, metadados pequenos e estruturas incrementais a caches grandes;
|
||||||
|
- evitar solucoes cuja performance dependa de assumir um host desktop mais forte do que o handheld alvo.
|
||||||
|
|
||||||
|
Direcao atual da discussao:
|
||||||
|
|
||||||
|
- nesta etapa, buffers extras devem ficar fora;
|
||||||
|
- o foco deve ser otimizar ao maximo o pipeline existente;
|
||||||
|
- cache intermediario ou novos buffers so entram em estudo depois que as otimizacoes de baixo custo de memoria forem medidas e esgotadas.
|
||||||
|
|
||||||
|
## Sugestao / Recomendacao Atual
|
||||||
|
|
||||||
|
Priorizar o estudo em camadas, nesta ordem:
|
||||||
|
|
||||||
|
1. instrumentar o pipeline por etapa;
|
||||||
|
2. validar dirty flags por subsistema como mecanismo minimo de invalidacao;
|
||||||
|
3. testar rebuild condicional de buckets e bypass/isolamento de fades;
|
||||||
|
4. otimizar primitivas e caminhos contiguos de escrita no framebuffer atual;
|
||||||
|
5. estudar separacao world/HUD e cache de layer apenas se os dados justificarem e o teto de memoria permitir;
|
||||||
|
5. tratar copy/convert do host como frente paralela de otimizacao, nao como substituto da analise do `gfx`.
|
||||||
|
|
||||||
|
## Achados Consolidados Ate Aqui
|
||||||
|
|
||||||
|
1. O contrato base continua sendo framebuffer destrutivo em memoria.
|
||||||
|
Draws escrevem diretamente no buffer de trabalho e operacoes posteriores sobrescrevem o que estiver por baixo. A agenda nao aponta para migracao a um renderer tipo GPU, retained mode ou compositor sofisticado.
|
||||||
|
|
||||||
|
2. O `present()` atual nao parece ser o problema principal.
|
||||||
|
No `gfx`, `present()` faz swap de buffers, nao copia completa de pixels.
|
||||||
|
|
||||||
|
3. O custo suspeito esta na recomposicao e na apresentacao, nao na manutencao do estado logico.
|
||||||
|
Escritas de `TileMap`, scroll e `GfxSetSprite` sao pequenas e pontuais; o custo potencialmente dominante esta em rasterizar pixels, aplicar fades fullscreen e converter o framebuffer para o host.
|
||||||
|
|
||||||
|
4. Os maiores suspeitos atuais de custo sao:
|
||||||
|
- rasterizacao de layers;
|
||||||
|
- rasterizacao de sprites;
|
||||||
|
- fades fullscreen;
|
||||||
|
- conversao `RGB565 -> RGBA8` no host;
|
||||||
|
- `pixels.render()` / apresentacao no host.
|
||||||
|
|
||||||
|
5. O pipeline atual deve ser otimizado antes de considerar buffers extras.
|
||||||
|
A direcao aceita da discussao e esgotar primeiro flags pequenas, bypasses, melhorias de primitive paths e ajustes incrementais no pipeline existente.
|
||||||
|
|
||||||
|
6. Restricao de plataforma pesa tanto quanto CPU.
|
||||||
|
Como o alvo e um handheld proprio com limitacoes de orcamento e memoria, solucoes que consumam RAM adicional significativa devem ficar fora desta fase.
|
||||||
|
|
||||||
|
7. A instrumentacao minima desejada para retomar o estudo ficou definida.
|
||||||
|
Quando houver workload representativo, queremos medir ao menos:
|
||||||
|
- `render_all_total_us`
|
||||||
|
- `bucket_rebuild_us`
|
||||||
|
- `layer_raster_us`
|
||||||
|
- `sprite_raster_us`
|
||||||
|
- `scene_fade_us`
|
||||||
|
- `hud_raster_us`
|
||||||
|
- `hud_fade_us`
|
||||||
|
- `host_convert_us`
|
||||||
|
- `host_present_us`
|
||||||
|
|
||||||
|
8. A retencao de metricas deve ser barata e orientada a analise posterior.
|
||||||
|
A preferencia atual e por acumuladores e ring buffer pequeno de snapshots, sem logging pesado por frame no hot path.
|
||||||
|
|
||||||
|
9. Ring buffer no `TileMap` nao e prioridade nesta fase.
|
||||||
|
O custo suspeito nao esta na manutencao da grade logica do mapa, e sim na composicao da janela visivel no framebuffer. Mudar a estrutura do mapa so faria sentido se o gargalo principal estivesse em streaming/atualizacao estrutural do cenario, o que nao e a hipotese atual.
|
||||||
|
|
||||||
|
10. A direcao de otimizacao mais promissora e padronizar copias massivas para o framebuffer atual.
|
||||||
|
Em vez de atacar apenas tilemaps com loops pixel a pixel, a discussao passa a favorecer um padrao geral de blit/copia massiva com:
|
||||||
|
- calculo previo de offsets e spans;
|
||||||
|
- fast paths para casos contiguos;
|
||||||
|
- trabalho por linha/chunk em vez de trabalho atomizado por pixel quando possivel;
|
||||||
|
- reaproveitamento dessa infraestrutura para tilemaps, sprites e outras operacoes de desenho destrutivo.
|
||||||
|
|
||||||
|
11. Migrar o `back` para `RGBA8888` nao e direcao aceita neste momento.
|
||||||
|
Isso aumentaria custo de memoria e largura de banda no alvo handheld, alem de deslocar a otimizacao para o host desktop. O contrato interno continua preferencialmente em `RGB565`, mesmo considerando fades e uma futura pipeline de lights.
|
||||||
|
|
||||||
|
## Nova Direcao Tecnica Em Estudo
|
||||||
|
|
||||||
|
Quando esta agenda for reaberta, a linha principal de investigacao deve considerar:
|
||||||
|
|
||||||
|
- manter o `TileMap` simples como estado logico;
|
||||||
|
- assumir que a janela visivel provavelmente continuara sendo recomposta na maior parte dos frames;
|
||||||
|
- concentrar a otimizacao em como essa recomposicao escreve no `back`;
|
||||||
|
- desenhar uma infraestrutura compartilhada de copias massivas/blits para o framebuffer atual;
|
||||||
|
- usar essa infraestrutura nao apenas para tilemaps, mas como base comum para raster de sprites e outros caminhos de desenho.
|
||||||
|
|
||||||
|
Pergunta orientadora da reabertura:
|
||||||
|
|
||||||
|
- como transformar o renderer de um pipeline de writes atomizados por pixel em um pipeline com fast paths de spans/chunks, sem perder a semantica de framebuffer destrutivo e sem pagar memoria extra relevante?
|
||||||
|
|
||||||
|
## Status de Standby
|
||||||
|
|
||||||
|
Esta agenda deve ficar em espera ate existir um game ou workload real que exercite de forma representativa:
|
||||||
|
|
||||||
|
- tilemaps com scroll e composicao de world;
|
||||||
|
- sprites em quantidade suficiente para testar buckets e overdraw;
|
||||||
|
- HUD ativo;
|
||||||
|
- fades;
|
||||||
|
- apresentacao completa no host.
|
||||||
|
|
||||||
|
Antes disso, qualquer conclusao sobre gargalo ou prioridade de otimizacao tende a ser prematura.
|
||||||
|
|
||||||
|
## Proximo Gatilho Para Reabrir
|
||||||
|
|
||||||
|
Reabrir esta agenda quando houver:
|
||||||
|
|
||||||
|
- um game jogavel ou cena de teste que percorra o pipeline completo;
|
||||||
|
- dados de workload mais proximos do uso real;
|
||||||
|
- necessidade concreta de justificar uma rodada de instrumentacao e profiling.
|
||||||
|
|
||||||
|
## Dependencias
|
||||||
|
|
||||||
|
- `../specs/04-gfx-peripheral.md`
|
||||||
|
- `../specs/11-portability-and-cross-platform-execution.md`
|
||||||
|
|
||||||
|
## Criterio de Saida Desta Agenda
|
||||||
|
|
||||||
|
Pode virar PR quando houver decisao escrita sobre:
|
||||||
|
|
||||||
|
- filosofia explicita de framebuffer destrutivo como contrato base;
|
||||||
|
- plano de instrumentacao para localizar o custo dominante do pipeline;
|
||||||
|
- nivel minimo de invalidacao no v1;
|
||||||
|
- politica de rebuild de buckets de sprites;
|
||||||
|
- bypass/cache de fade e HUD;
|
||||||
|
- politica para otimizar primitivas sem mudar a semantica observavel;
|
||||||
|
- meta de custo para o render pipeline.
|
||||||
@ -127,31 +127,7 @@ 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
|
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.
|
typed failure if the worker cannot join within the configured timeout.
|
||||||
|
|
||||||
## 8 Async Asset and IO Work Lane
|
## 8 Determinism and Best Practices
|
||||||
|
|
||||||
The asset/IO async work lane is not a machine-visible event source and does not
|
|
||||||
introduce guest callbacks. It is an implementation-side lane for asset
|
|
||||||
IO/decode/materialization and compatible persistence IO work.
|
|
||||||
|
|
||||||
The lane is serial:
|
|
||||||
|
|
||||||
- it has at most one active job;
|
|
||||||
- it keeps an ordered backlog;
|
|
||||||
- it is separate from the render worker;
|
|
||||||
- it must not create one OS thread per guest-visible asset request.
|
|
||||||
|
|
||||||
Asset jobs are keyed by target `bank_type/slot`. A newer request for the same
|
|
||||||
target supersedes the older request. Superseding is an operational status and
|
|
||||||
does not execute guest code.
|
|
||||||
|
|
||||||
Asset install/commit into resident banks happens on the main runtime lane at
|
|
||||||
predictable ownership points. The async lane prepares materialized results; it
|
|
||||||
does not publish resident graphics/audio/scene state directly.
|
|
||||||
|
|
||||||
FS and game persistence services may consume this lane for IO-style work, but
|
|
||||||
public FS API shape is defined by the FS/app-home contract, not by this chapter.
|
|
||||||
|
|
||||||
## 9 Determinism and Best Practices
|
|
||||||
|
|
||||||
PROMETEU encourages:
|
PROMETEU encourages:
|
||||||
|
|
||||||
@ -166,7 +142,7 @@ PROMETEU discourages:
|
|||||||
- hidden timing channels;
|
- hidden timing channels;
|
||||||
- ambiguous out-of-band execution.
|
- ambiguous out-of-band execution.
|
||||||
|
|
||||||
## 10 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.
|
- [`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.
|
- [`10-debug-inspection-and-profiling.md`](10-debug-inspection-and-profiling.md) defines observability and diagnostics surfaces.
|
||||||
|
|||||||
@ -393,129 +393,14 @@ Fault boundary:
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- `handle` is returned when `load` status is `OK`;
|
- `handle` is valid only when `load` status is `OK`;
|
||||||
- `handle` represents a stable bank slot target, not a worker thread;
|
|
||||||
- a known handle remains queryable even when the slot is empty, has no active
|
|
||||||
request, or has a superseded request;
|
|
||||||
- failed `load` returns `handle = 0`;
|
- failed `load` returns `handle = 0`;
|
||||||
- `commit` and `cancel` must not be silent no-op for unknown/invalid handle state.
|
- `commit` and `cancel` must not be silent no-op for unknown/invalid handle state.
|
||||||
- `asset.load` resolves the target bank type from `asset_table` using `asset_id`;
|
- `asset.load` resolves the target bank type from `asset_table` using `asset_id`;
|
||||||
- public callers must not supply `asset_name` or `bank_type` to `asset.load`;
|
- public callers must not supply `asset_name` or `bank_type` to `asset.load`;
|
||||||
- slot validation and residency/lifecycle rejection remain in `asset` status space and are not delegated to `bank`.
|
- slot validation and residency/lifecycle rejection remain in `asset` status space and are not delegated to `bank`.
|
||||||
|
|
||||||
### 11.2 Async work lane and backlog
|
### 11.2 Minimum status tables
|
||||||
|
|
||||||
Asset loading uses the runtime async work lane. This lane is separate from the
|
|
||||||
VM/main lane and separate from the render worker lane.
|
|
||||||
|
|
||||||
The asset async lane is serial:
|
|
||||||
|
|
||||||
- it executes at most one active asset job at a time;
|
|
||||||
- it keeps an ordered backlog of pending requests;
|
|
||||||
- it must not create one OS thread per `asset.load` request.
|
|
||||||
|
|
||||||
Asset requests are keyed by the target `bank_type/slot`.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- each `bank_type/slot` has at most one current request;
|
|
||||||
- a newer request for the same `bank_type/slot` supersedes the previous request;
|
|
||||||
- if the previous request is queued, it is removed from the backlog;
|
|
||||||
- if the previous request is active, the lane should cancel cooperatively when
|
|
||||||
the current phase supports cheap cancellation;
|
|
||||||
- if active work cannot stop cheaply, it may finish, but the result must be
|
|
||||||
discarded when its generation no longer matches the target request generation;
|
|
||||||
- if the target already contains the requested `asset_id` as a valid resident
|
|
||||||
asset, `asset.load` returns a ready handle without adding a backlog entry.
|
|
||||||
|
|
||||||
The effective backlog size is bounded by the sum of targetable bank slots,
|
|
||||||
because only one current request can exist per `bank_type/slot`. The runtime
|
|
||||||
does not expose a guest-visible `queue_full` status for the normal asset
|
|
||||||
backlog path.
|
|
||||||
|
|
||||||
### 11.3 Handle state
|
|
||||||
|
|
||||||
An asset handle observes one stable bank slot target. Its observable state has
|
|
||||||
two parts:
|
|
||||||
|
|
||||||
```text
|
|
||||||
handle:
|
|
||||||
bank_type
|
|
||||||
slot
|
|
||||||
|
|
||||||
slot_state:
|
|
||||||
loaded_asset_id
|
|
||||||
resident_state
|
|
||||||
slot_generation
|
|
||||||
|
|
||||||
request_state:
|
|
||||||
requested_asset_id
|
|
||||||
request_generation
|
|
||||||
state
|
|
||||||
backlog_position
|
|
||||||
progress
|
|
||||||
```
|
|
||||||
|
|
||||||
`slot_state` describes what is currently resident in the target slot.
|
|
||||||
`request_state` describes the current or most recent request for that target.
|
|
||||||
|
|
||||||
Mutating operations such as commit, cancel, promote, demote, and move must act
|
|
||||||
on the current request generation. They must not accidentally mutate a newer
|
|
||||||
request through an older handle view.
|
|
||||||
|
|
||||||
### 11.4 Backlog inspection and ordering
|
|
||||||
|
|
||||||
The asset backlog surface may expose these status-first operations:
|
|
||||||
|
|
||||||
- `asset.backlog_info() -> (status, pending_count, active_handle, active_asset_id, active_bank_type, active_slot, active_progress)`
|
|
||||||
- `asset.backlog_position(handle) -> (status, state, position, progress)`
|
|
||||||
- `asset.backlog_move(handle, new_position) -> status`
|
|
||||||
- `asset.backlog_promote(handle) -> status`
|
|
||||||
- `asset.backlog_demote(handle) -> status`
|
|
||||||
- `asset.target_status(bank_type, slot) -> (status, asset_id, handle, state, position, progress)`
|
|
||||||
|
|
||||||
`asset.backlog_promote(handle)` is a shortcut for moving a queued request to
|
|
||||||
position `1`, the first pending position after the active job.
|
|
||||||
|
|
||||||
`asset.backlog_demote(handle)` is a shortcut for moving a queued request to the
|
|
||||||
end of the pending backlog.
|
|
||||||
|
|
||||||
### 11.5 Progress and telemetry
|
|
||||||
|
|
||||||
Asset progress uses integer progress, not floating point. The preferred scale is
|
|
||||||
`0..10000`.
|
|
||||||
|
|
||||||
The initial phase model is:
|
|
||||||
|
|
||||||
```text
|
|
||||||
queued -> 0
|
|
||||||
read -> 0..4000
|
|
||||||
decode -> 4000..9000
|
|
||||||
stage -> 9000..10000
|
|
||||||
ready -> 10000
|
|
||||||
```
|
|
||||||
|
|
||||||
If a phase cannot report internal progress, it keeps the previous progress mark
|
|
||||||
and advances at phase completion. The runtime must not invent false precision
|
|
||||||
for non-linear decode phases.
|
|
||||||
|
|
||||||
Minimum telemetry:
|
|
||||||
|
|
||||||
- current backlog depth;
|
|
||||||
- target/request position;
|
|
||||||
- active job progress;
|
|
||||||
- jobs submitted;
|
|
||||||
- jobs completed;
|
|
||||||
- jobs failed;
|
|
||||||
- jobs canceled;
|
|
||||||
- jobs superseded;
|
|
||||||
- job duration;
|
|
||||||
- percentiles by `bank_type`;
|
|
||||||
- lightweight percentiles or small-window samples by `asset_id`.
|
|
||||||
|
|
||||||
Percentiles are updated when a job closes, not inside the inner decode loop.
|
|
||||||
|
|
||||||
### 11.6 Minimum status tables
|
|
||||||
|
|
||||||
`asset.load` request statuses:
|
`asset.load` request statuses:
|
||||||
|
|
||||||
@ -533,16 +418,9 @@ Percentiles are updated when a job closes, not inside the inner decode loop.
|
|||||||
- `4` = `CANCELED`
|
- `4` = `CANCELED`
|
||||||
- `5` = `ERROR`
|
- `5` = `ERROR`
|
||||||
- `6` = `UNKNOWN_HANDLE`
|
- `6` = `UNKNOWN_HANDLE`
|
||||||
- `7` = `QUEUED`
|
|
||||||
- `8` = `ACTIVE`
|
|
||||||
- `9` = `SUPERSEDED`
|
|
||||||
- `10` = `EMPTY`
|
|
||||||
- `11` = `INVALID`
|
|
||||||
- `12` = `BACKEND_UNAVAILABLE`
|
|
||||||
|
|
||||||
`asset.commit` and `asset.cancel` operation statuses:
|
`asset.commit` and `asset.cancel` operation statuses:
|
||||||
|
|
||||||
- `0` = `OK`
|
- `0` = `OK`
|
||||||
- `1` = `UNKNOWN_HANDLE`
|
- `1` = `UNKNOWN_HANDLE`
|
||||||
- `2` = `INVALID_STATE`
|
- `2` = `INVALID_STATE`
|
||||||
- `3` = `SUPERSEDED`
|
|
||||||
|
|||||||
@ -203,12 +203,6 @@ Canonical operations in v1 are:
|
|||||||
- `asset.status(handle) -> status`
|
- `asset.status(handle) -> status`
|
||||||
- `asset.commit(handle) -> status`
|
- `asset.commit(handle) -> status`
|
||||||
- `asset.cancel(handle) -> status`
|
- `asset.cancel(handle) -> status`
|
||||||
- `asset.backlog_info() -> (status, pending_count, active_handle, active_asset_id, active_bank_type, active_slot, active_progress)`
|
|
||||||
- `asset.backlog_position(handle) -> (status, state, position, progress)`
|
|
||||||
- `asset.backlog_move(handle, new_position) -> status`
|
|
||||||
- `asset.backlog_promote(handle) -> status`
|
|
||||||
- `asset.backlog_demote(handle) -> status`
|
|
||||||
- `asset.target_status(bank_type, slot) -> (status, asset_id, handle, state, position, progress)`
|
|
||||||
|
|
||||||
For `asset.load`:
|
For `asset.load`:
|
||||||
|
|
||||||
@ -216,18 +210,6 @@ For `asset.load`:
|
|||||||
- `slot` is the target slot index;
|
- `slot` is the target slot index;
|
||||||
- bank kind is resolved from `asset_table` by `asset_id`, not supplied by the caller.
|
- bank kind is resolved from `asset_table` by `asset_id`, not supplied by the caller.
|
||||||
|
|
||||||
Asset handles represent stable bank slot targets. A handle can be queried even
|
|
||||||
when its slot has no resident asset or active request. Internally the handle
|
|
||||||
state separates resident `slot_state` from current `request_state`.
|
|
||||||
|
|
||||||
The asset backlog is keyed by `bank_type/slot`. New requests for the same target
|
|
||||||
supersede older requests for that target. `superseded` is an operational status,
|
|
||||||
not a structural trap.
|
|
||||||
|
|
||||||
`asset.backlog_promote(handle)` and `asset.backlog_demote(handle)` are
|
|
||||||
convenience operations over backlog movement. They do not introduce a second
|
|
||||||
ordering model.
|
|
||||||
|
|
||||||
### Bank diagnostics surface (`bank`, v1)
|
### Bank diagnostics surface (`bank`, v1)
|
||||||
|
|
||||||
`DEC-0009` narrows the public bank contract:
|
`DEC-0009` narrows the public bank contract:
|
||||||
|
|||||||
@ -44,9 +44,6 @@ Normal operational success and operational failure conditions should be represen
|
|||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- asset not yet loaded;
|
- asset not yet loaded;
|
||||||
- asset request queued or active;
|
|
||||||
- asset request superseded by a newer request for the same bank slot;
|
|
||||||
- asset backend unavailable;
|
|
||||||
- audio voice unavailable;
|
- audio voice unavailable;
|
||||||
- persistent storage full.
|
- persistent storage full.
|
||||||
|
|
||||||
@ -110,11 +107,6 @@ Game memcard operations (`mem.*`) are status-first and use `fs` capability in v1
|
|||||||
`mem` remains layered on runtime `fs`; no parallel persistence channel is introduced.
|
`mem` remains layered on runtime `fs`; no parallel persistence channel is introduced.
|
||||||
Domain surface, status catalog and slot semantics are defined in [`08-save-memory-and-memcard.md`](08-save-memory-and-memcard.md).
|
Domain surface, status catalog and slot semantics are defined in [`08-save-memory-and-memcard.md`](08-save-memory-and-memcard.md).
|
||||||
|
|
||||||
Asset backlog operations are status-first. `queued`, `active`, `ready`,
|
|
||||||
`canceled`, `superseded`, `empty`, `invalid`, decode failure, and backend
|
|
||||||
unavailability are operational results. They must not be reclassified as `Trap`
|
|
||||||
unless the caller violates the structural ABI contract.
|
|
||||||
|
|
||||||
## 3 Interaction with the Garbage Collector
|
## 3 Interaction with the Garbage Collector
|
||||||
|
|
||||||
The VM heap and host-managed memory are separate.
|
The VM heap and host-managed memory are separate.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user