implements PLN-0125 asset backlog stable handles
This commit is contained in:
parent
7598a15782
commit
30c24a4ea7
@ -18,10 +18,10 @@ use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
|||||||
use prometeu_hal::sound_bank::SoundBank;
|
use prometeu_hal::sound_bank::SoundBank;
|
||||||
use prometeu_hal::tile::Tile;
|
use prometeu_hal::tile::Tile;
|
||||||
use prometeu_hal::tilemap::TileMap;
|
use prometeu_hal::tilemap::TileMap;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Condvar, Mutex, RwLock};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@ -31,6 +31,8 @@ type StagingMap<T> = HashMap<HandleId, StagedValue<T>>;
|
|||||||
|
|
||||||
type AssetTable = HashMap<AssetId, AssetEntry>;
|
type AssetTable = HashMap<AssetId, AssetEntry>;
|
||||||
type HandleTable = HashMap<HandleId, LoadHandleInfo>;
|
type HandleTable = HashMap<HandleId, LoadHandleInfo>;
|
||||||
|
type TargetHandleTable = HashMap<SlotRef, HandleId>;
|
||||||
|
type TargetGenerationTable = HashMap<SlotRef, u64>;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct GlyphAssetSlotIndex {
|
pub struct GlyphAssetSlotIndex {
|
||||||
@ -182,6 +184,8 @@ impl<T> BankPolicy<T> {
|
|||||||
pub struct AssetManager {
|
pub struct AssetManager {
|
||||||
assets: Arc<RwLock<AssetTable>>,
|
assets: Arc<RwLock<AssetTable>>,
|
||||||
handles: Arc<RwLock<HandleTable>>,
|
handles: Arc<RwLock<HandleTable>>,
|
||||||
|
target_handles: Arc<RwLock<TargetHandleTable>>,
|
||||||
|
target_generations: Arc<RwLock<TargetGenerationTable>>,
|
||||||
next_handle_id: Mutex<HandleId>,
|
next_handle_id: Mutex<HandleId>,
|
||||||
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
||||||
|
|
||||||
@ -203,14 +207,123 @@ pub struct AssetManager {
|
|||||||
/// Residency policy for scene banks.
|
/// Residency policy for scene banks.
|
||||||
scene_policy: BankPolicy<SceneBank>,
|
scene_policy: BankPolicy<SceneBank>,
|
||||||
|
|
||||||
|
load_worker: AssetLoadWorker,
|
||||||
|
|
||||||
// Commits that are ready to be applied at the next frame boundary.
|
// Commits that are ready to be applied at the next frame boundary.
|
||||||
pending_commits: Mutex<Vec<HandleId>>,
|
pending_commits: Mutex<Vec<(HandleId, u64)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LoadHandleInfo {
|
struct LoadHandleInfo {
|
||||||
_asset_id: AssetId,
|
_asset_id: AssetId,
|
||||||
slot: SlotRef,
|
slot: SlotRef,
|
||||||
status: LoadStatus,
|
status: LoadStatus,
|
||||||
|
request_generation: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AssetLoadJob {
|
||||||
|
handle_id: HandleId,
|
||||||
|
asset_id: AssetId,
|
||||||
|
slot: SlotRef,
|
||||||
|
request_generation: u64,
|
||||||
|
entry: AssetEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct AssetLoadQueueState {
|
||||||
|
pending: VecDeque<AssetLoadJob>,
|
||||||
|
shutdown: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct AssetLoadQueue {
|
||||||
|
state: Mutex<AssetLoadQueueState>,
|
||||||
|
ready: Condvar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetLoadQueue {
|
||||||
|
fn submit(&self, job: AssetLoadJob) {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
if state.shutdown {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.pending.push_back(job);
|
||||||
|
self.ready.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_job(&self) -> Option<AssetLoadJob> {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
loop {
|
||||||
|
if let Some(job) = state.pending.pop_front() {
|
||||||
|
return Some(job);
|
||||||
|
}
|
||||||
|
if state.shutdown {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
state = self.ready.wait(state).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_pending(&self) {
|
||||||
|
self.state.lock().unwrap().pending.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown(&self) {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.pending.clear();
|
||||||
|
state.shutdown = true;
|
||||||
|
self.ready.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AssetLoadWorker {
|
||||||
|
queue: Arc<AssetLoadQueue>,
|
||||||
|
join_handle: Option<thread::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetLoadWorker {
|
||||||
|
fn new(
|
||||||
|
handles: Arc<RwLock<HandleTable>>,
|
||||||
|
target_generations: Arc<RwLock<TargetGenerationTable>>,
|
||||||
|
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
||||||
|
gfx_policy: BankPolicy<GlyphBank>,
|
||||||
|
sound_policy: BankPolicy<SoundBank>,
|
||||||
|
scene_policy: BankPolicy<SceneBank>,
|
||||||
|
) -> Self {
|
||||||
|
let queue = Arc::new(AssetLoadQueue::default());
|
||||||
|
let worker_queue = Arc::clone(&queue);
|
||||||
|
let join_handle = thread::spawn(move || {
|
||||||
|
while let Some(job) = worker_queue.next_job() {
|
||||||
|
AssetManager::process_load_job(
|
||||||
|
job,
|
||||||
|
&handles,
|
||||||
|
&target_generations,
|
||||||
|
&assets_data,
|
||||||
|
&gfx_policy,
|
||||||
|
&sound_policy,
|
||||||
|
&scene_policy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { queue, join_handle: Some(join_handle) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(&self, job: AssetLoadJob) {
|
||||||
|
self.queue.submit(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_pending(&self) {
|
||||||
|
self.queue.clear_pending();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for AssetLoadWorker {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.queue.shutdown();
|
||||||
|
if let Some(join_handle) = self.join_handle.take() {
|
||||||
|
let _ = join_handle.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -333,6 +446,21 @@ impl AssetManager {
|
|||||||
asset_map.insert(entry.asset_id, entry);
|
asset_map.insert(entry.asset_id, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let handles = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
let target_generations = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
let assets_data = Arc::new(RwLock::new(assets_data));
|
||||||
|
let gfx_policy = BankPolicy::new();
|
||||||
|
let sound_policy = BankPolicy::new();
|
||||||
|
let scene_policy = BankPolicy::new();
|
||||||
|
let load_worker = AssetLoadWorker::new(
|
||||||
|
Arc::clone(&handles),
|
||||||
|
Arc::clone(&target_generations),
|
||||||
|
Arc::clone(&assets_data),
|
||||||
|
gfx_policy.clone(),
|
||||||
|
sound_policy.clone(),
|
||||||
|
scene_policy.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
assets: Arc::new(RwLock::new(asset_map)),
|
assets: Arc::new(RwLock::new(asset_map)),
|
||||||
gfx_installer,
|
gfx_installer,
|
||||||
@ -342,12 +470,15 @@ impl AssetManager {
|
|||||||
glyph_slot_index: GlyphAssetSlotIndex::new(),
|
glyph_slot_index: GlyphAssetSlotIndex::new(),
|
||||||
sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||||
scene_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
scene_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||||
gfx_policy: BankPolicy::new(),
|
gfx_policy,
|
||||||
sound_policy: BankPolicy::new(),
|
sound_policy,
|
||||||
scene_policy: BankPolicy::new(),
|
scene_policy,
|
||||||
handles: Arc::new(RwLock::new(HashMap::new())),
|
handles,
|
||||||
|
target_handles: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
target_generations,
|
||||||
next_handle_id: Mutex::new(1),
|
next_handle_id: Mutex::new(1),
|
||||||
assets_data: Arc::new(RwLock::new(assets_data)),
|
assets_data,
|
||||||
|
load_worker,
|
||||||
pending_commits: Mutex::new(Vec::new()),
|
pending_commits: Mutex::new(Vec::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -435,6 +566,51 @@ impl AssetManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_for_slot(&self, slot: SlotRef) -> HandleId {
|
||||||
|
if let Some(handle_id) = self.target_handles.read().unwrap().get(&slot).copied() {
|
||||||
|
return handle_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut target_handles = self.target_handles.write().unwrap();
|
||||||
|
if let Some(handle_id) = target_handles.get(&slot).copied() {
|
||||||
|
return handle_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut next_id = self.next_handle_id.lock().unwrap();
|
||||||
|
let handle_id = *next_id;
|
||||||
|
*next_id += 1;
|
||||||
|
target_handles.insert(slot, handle_id);
|
||||||
|
handle_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bump_target_generation(&self, slot: SlotRef) -> u64 {
|
||||||
|
let mut generations = self.target_generations.write().unwrap();
|
||||||
|
let generation = generations.entry(slot).or_insert(0);
|
||||||
|
*generation += 1;
|
||||||
|
*generation
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_target_generation(
|
||||||
|
target_generations: &Arc<RwLock<TargetGenerationTable>>,
|
||||||
|
slot: SlotRef,
|
||||||
|
) -> u64 {
|
||||||
|
target_generations.read().unwrap().get(&slot).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_current_target_generation(
|
||||||
|
target_generations: &Arc<RwLock<TargetGenerationTable>>,
|
||||||
|
slot: SlotRef,
|
||||||
|
request_generation: u64,
|
||||||
|
) -> bool {
|
||||||
|
Self::current_target_generation(target_generations, slot) == request_generation
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_staging_for_handle(&self, handle_id: HandleId) {
|
||||||
|
self.gfx_policy.take_staging(handle_id);
|
||||||
|
self.sound_policy.take_staging(handle_id);
|
||||||
|
self.scene_policy.take_staging(handle_id);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError> {
|
pub fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError> {
|
||||||
if slot_index >= 16 {
|
if slot_index >= 16 {
|
||||||
return Err(AssetLoadError::SlotIndexInvalid);
|
return Err(AssetLoadError::SlotIndexInvalid);
|
||||||
@ -449,9 +625,9 @@ impl AssetManager {
|
|||||||
BankType::SCENE => SlotRef::scene(slot_index),
|
BankType::SCENE => SlotRef::scene(slot_index),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut next_id = self.next_handle_id.lock().unwrap();
|
let handle_id = self.handle_for_slot(slot);
|
||||||
let handle_id = *next_id;
|
let request_generation = self.bump_target_generation(slot);
|
||||||
*next_id += 1;
|
self.clear_staging_for_handle(handle_id);
|
||||||
|
|
||||||
// Check if already resident (Dedup)
|
// Check if already resident (Dedup)
|
||||||
let already_resident = match entry.bank_type {
|
let already_resident = match entry.bank_type {
|
||||||
@ -484,7 +660,12 @@ impl AssetManager {
|
|||||||
if already_resident {
|
if already_resident {
|
||||||
self.handles.write().unwrap().insert(
|
self.handles.write().unwrap().insert(
|
||||||
handle_id,
|
handle_id,
|
||||||
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::READY },
|
LoadHandleInfo {
|
||||||
|
_asset_id: asset_id,
|
||||||
|
slot,
|
||||||
|
status: LoadStatus::READY,
|
||||||
|
request_generation,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return Ok(handle_id);
|
return Ok(handle_id);
|
||||||
}
|
}
|
||||||
@ -492,124 +673,173 @@ impl AssetManager {
|
|||||||
// Not resident, start loading
|
// Not resident, start loading
|
||||||
self.handles.write().unwrap().insert(
|
self.handles.write().unwrap().insert(
|
||||||
handle_id,
|
handle_id,
|
||||||
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::PENDING },
|
LoadHandleInfo {
|
||||||
|
_asset_id: asset_id,
|
||||||
|
slot,
|
||||||
|
status: LoadStatus::PENDING,
|
||||||
|
request_generation,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let handles = self.handles.clone();
|
self.load_worker.submit(AssetLoadJob {
|
||||||
let assets_data = self.assets_data.clone();
|
handle_id,
|
||||||
let entry_clone = entry.clone();
|
asset_id,
|
||||||
|
slot,
|
||||||
// Capture policies for the worker thread
|
request_generation,
|
||||||
let gfx_policy = self.gfx_policy.clone();
|
entry,
|
||||||
let sound_policy = self.sound_policy.clone();
|
|
||||||
let scene_policy = self.scene_policy.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
// Update status to LOADING
|
|
||||||
{
|
|
||||||
let mut handles_map = handles.write().unwrap();
|
|
||||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
|
||||||
if h.status == LoadStatus::PENDING {
|
|
||||||
h.status = LoadStatus::LOADING;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match entry_clone.bank_type {
|
|
||||||
BankType::GLYPH => {
|
|
||||||
let result = Self::perform_load_glyph_bank(&entry_clone, assets_data);
|
|
||||||
if let Ok(tilebank) = result {
|
|
||||||
let bank_arc = Arc::new(tilebank);
|
|
||||||
let resident_arc = gfx_policy.put_resident(
|
|
||||||
asset_id,
|
|
||||||
bank_arc,
|
|
||||||
entry_clone.decoded_size as usize,
|
|
||||||
);
|
|
||||||
gfx_policy.stage(
|
|
||||||
handle_id,
|
|
||||||
resident_arc,
|
|
||||||
entry_clone.decoded_size as usize,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut handles_map = handles.write().unwrap();
|
|
||||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
|
||||||
if h.status == LoadStatus::LOADING {
|
|
||||||
h.status = LoadStatus::READY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut handles_map = handles.write().unwrap();
|
|
||||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
|
||||||
h.status = LoadStatus::ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BankType::SOUNDS => {
|
|
||||||
let result = Self::perform_load_sound_bank(&entry_clone, assets_data);
|
|
||||||
if let Ok(soundbank) = result {
|
|
||||||
let bank_arc = Arc::new(soundbank);
|
|
||||||
let resident_arc = sound_policy.put_resident(
|
|
||||||
asset_id,
|
|
||||||
bank_arc,
|
|
||||||
entry_clone.decoded_size as usize,
|
|
||||||
);
|
|
||||||
sound_policy.stage(
|
|
||||||
handle_id,
|
|
||||||
resident_arc,
|
|
||||||
entry_clone.decoded_size as usize,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut handles_map = handles.write().unwrap();
|
|
||||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
|
||||||
if h.status == LoadStatus::LOADING {
|
|
||||||
h.status = LoadStatus::READY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut handles_map = handles.write().unwrap();
|
|
||||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
|
||||||
h.status = LoadStatus::ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BankType::SCENE => {
|
|
||||||
let result = Self::perform_load_scene_bank(&entry_clone, assets_data);
|
|
||||||
if let Ok(scenebank) = result {
|
|
||||||
let bank_arc = Arc::new(scenebank);
|
|
||||||
let resident_arc = scene_policy.put_resident(
|
|
||||||
asset_id,
|
|
||||||
bank_arc,
|
|
||||||
entry_clone.decoded_size as usize,
|
|
||||||
);
|
|
||||||
scene_policy.stage(
|
|
||||||
handle_id,
|
|
||||||
resident_arc,
|
|
||||||
entry_clone.decoded_size as usize,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut handles_map = handles.write().unwrap();
|
|
||||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
|
||||||
if h.status == LoadStatus::LOADING {
|
|
||||||
h.status = LoadStatus::READY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut handles_map = handles.write().unwrap();
|
|
||||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
|
||||||
h.status = LoadStatus::ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(handle_id)
|
Ok(handle_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process_load_job(
|
||||||
|
job: AssetLoadJob,
|
||||||
|
handles: &Arc<RwLock<HandleTable>>,
|
||||||
|
target_generations: &Arc<RwLock<TargetGenerationTable>>,
|
||||||
|
assets_data: &Arc<RwLock<AssetsPayloadSource>>,
|
||||||
|
gfx_policy: &BankPolicy<GlyphBank>,
|
||||||
|
sound_policy: &BankPolicy<SoundBank>,
|
||||||
|
scene_policy: &BankPolicy<SceneBank>,
|
||||||
|
) {
|
||||||
|
if !Self::is_current_target_generation(target_generations, job.slot, job.request_generation)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut handles_map = handles.write().unwrap();
|
||||||
|
let Some(handle) = handles_map.get_mut(&job.handle_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if handle.request_generation != job.request_generation
|
||||||
|
|| handle.status != LoadStatus::PENDING
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handle.status = LoadStatus::LOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
match job.entry.bank_type {
|
||||||
|
BankType::GLYPH => {
|
||||||
|
let result = Self::perform_load_glyph_bank(&job.entry, Arc::clone(assets_data));
|
||||||
|
match result {
|
||||||
|
Ok(tilebank) => {
|
||||||
|
if !Self::is_current_target_generation(
|
||||||
|
target_generations,
|
||||||
|
job.slot,
|
||||||
|
job.request_generation,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bank_arc = Arc::new(tilebank);
|
||||||
|
let resident_arc = gfx_policy.put_resident(
|
||||||
|
job.asset_id,
|
||||||
|
bank_arc,
|
||||||
|
job.entry.decoded_size as usize,
|
||||||
|
);
|
||||||
|
if !Self::is_current_target_generation(
|
||||||
|
target_generations,
|
||||||
|
job.slot,
|
||||||
|
job.request_generation,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gfx_policy.stage(
|
||||||
|
job.handle_id,
|
||||||
|
resident_arc,
|
||||||
|
job.entry.decoded_size as usize,
|
||||||
|
);
|
||||||
|
Self::complete_load_job(handles, &job, LoadStatus::READY);
|
||||||
|
}
|
||||||
|
Err(_) => Self::complete_load_job(handles, &job, LoadStatus::ERROR),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BankType::SOUNDS => {
|
||||||
|
let result = Self::perform_load_sound_bank(&job.entry, Arc::clone(assets_data));
|
||||||
|
match result {
|
||||||
|
Ok(soundbank) => {
|
||||||
|
if !Self::is_current_target_generation(
|
||||||
|
target_generations,
|
||||||
|
job.slot,
|
||||||
|
job.request_generation,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bank_arc = Arc::new(soundbank);
|
||||||
|
let resident_arc = sound_policy.put_resident(
|
||||||
|
job.asset_id,
|
||||||
|
bank_arc,
|
||||||
|
job.entry.decoded_size as usize,
|
||||||
|
);
|
||||||
|
if !Self::is_current_target_generation(
|
||||||
|
target_generations,
|
||||||
|
job.slot,
|
||||||
|
job.request_generation,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sound_policy.stage(
|
||||||
|
job.handle_id,
|
||||||
|
resident_arc,
|
||||||
|
job.entry.decoded_size as usize,
|
||||||
|
);
|
||||||
|
Self::complete_load_job(handles, &job, LoadStatus::READY);
|
||||||
|
}
|
||||||
|
Err(_) => Self::complete_load_job(handles, &job, LoadStatus::ERROR),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BankType::SCENE => {
|
||||||
|
let result = Self::perform_load_scene_bank(&job.entry, Arc::clone(assets_data));
|
||||||
|
match result {
|
||||||
|
Ok(scenebank) => {
|
||||||
|
if !Self::is_current_target_generation(
|
||||||
|
target_generations,
|
||||||
|
job.slot,
|
||||||
|
job.request_generation,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bank_arc = Arc::new(scenebank);
|
||||||
|
let resident_arc = scene_policy.put_resident(
|
||||||
|
job.asset_id,
|
||||||
|
bank_arc,
|
||||||
|
job.entry.decoded_size as usize,
|
||||||
|
);
|
||||||
|
if !Self::is_current_target_generation(
|
||||||
|
target_generations,
|
||||||
|
job.slot,
|
||||||
|
job.request_generation,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scene_policy.stage(
|
||||||
|
job.handle_id,
|
||||||
|
resident_arc,
|
||||||
|
job.entry.decoded_size as usize,
|
||||||
|
);
|
||||||
|
Self::complete_load_job(handles, &job, LoadStatus::READY);
|
||||||
|
}
|
||||||
|
Err(_) => Self::complete_load_job(handles, &job, LoadStatus::ERROR),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_load_job(
|
||||||
|
handles: &Arc<RwLock<HandleTable>>,
|
||||||
|
job: &AssetLoadJob,
|
||||||
|
status: LoadStatus,
|
||||||
|
) {
|
||||||
|
let mut handles_map = handles.write().unwrap();
|
||||||
|
if let Some(handle) = handles_map.get_mut(&job.handle_id) {
|
||||||
|
if handle.request_generation == job.request_generation
|
||||||
|
&& handle.status == LoadStatus::LOADING
|
||||||
|
{
|
||||||
|
handle.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn perform_load_glyph_bank(
|
fn perform_load_glyph_bank(
|
||||||
entry: &AssetEntry,
|
entry: &AssetEntry,
|
||||||
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
||||||
@ -960,7 +1190,7 @@ impl AssetManager {
|
|||||||
return AssetOpStatus::UnknownHandle;
|
return AssetOpStatus::UnknownHandle;
|
||||||
};
|
};
|
||||||
if h.status == LoadStatus::READY {
|
if h.status == LoadStatus::READY {
|
||||||
self.pending_commits.lock().unwrap().push(handle);
|
self.pending_commits.lock().unwrap().push((handle, h.request_generation));
|
||||||
AssetOpStatus::Ok
|
AssetOpStatus::Ok
|
||||||
} else {
|
} else {
|
||||||
AssetOpStatus::InvalidState
|
AssetOpStatus::InvalidState
|
||||||
@ -969,23 +1199,30 @@ impl AssetManager {
|
|||||||
|
|
||||||
pub fn cancel(&self, handle: HandleId) -> AssetOpStatus {
|
pub fn cancel(&self, handle: HandleId) -> AssetOpStatus {
|
||||||
let mut final_status = AssetOpStatus::UnknownHandle;
|
let mut final_status = AssetOpStatus::UnknownHandle;
|
||||||
let mut handles_map = self.handles.write().unwrap();
|
let mut canceled_slot = None;
|
||||||
if let Some(h) = handles_map.get_mut(&handle) {
|
{
|
||||||
final_status = match h.status {
|
let mut handles_map = self.handles.write().unwrap();
|
||||||
LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => AssetOpStatus::Ok,
|
if let Some(h) = handles_map.get_mut(&handle) {
|
||||||
LoadStatus::CANCELED => AssetOpStatus::Ok,
|
final_status = match h.status {
|
||||||
_ => AssetOpStatus::InvalidState,
|
LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => {
|
||||||
};
|
AssetOpStatus::Ok
|
||||||
match h.status {
|
}
|
||||||
LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => {
|
LoadStatus::CANCELED => AssetOpStatus::Ok,
|
||||||
h.status = LoadStatus::CANCELED;
|
_ => AssetOpStatus::InvalidState,
|
||||||
|
};
|
||||||
|
match h.status {
|
||||||
|
LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => {
|
||||||
|
h.status = LoadStatus::CANCELED;
|
||||||
|
canceled_slot = Some(h.slot);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.gfx_policy.take_staging(handle);
|
if let Some(slot) = canceled_slot {
|
||||||
self.sound_policy.take_staging(handle);
|
self.bump_target_generation(slot);
|
||||||
self.scene_policy.take_staging(handle);
|
}
|
||||||
|
self.clear_staging_for_handle(handle);
|
||||||
final_status
|
final_status
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -993,9 +1230,9 @@ impl AssetManager {
|
|||||||
let mut pending = self.pending_commits.lock().unwrap();
|
let mut pending = self.pending_commits.lock().unwrap();
|
||||||
let mut handles = self.handles.write().unwrap();
|
let mut handles = self.handles.write().unwrap();
|
||||||
|
|
||||||
for handle_id in pending.drain(..) {
|
for (handle_id, request_generation) in pending.drain(..) {
|
||||||
if let Some(h) = handles.get_mut(&handle_id) {
|
if let Some(h) = handles.get_mut(&handle_id) {
|
||||||
if h.status == LoadStatus::READY {
|
if h.status == LoadStatus::READY && h.request_generation == request_generation {
|
||||||
match h.slot.asset_type {
|
match h.slot.asset_type {
|
||||||
BankType::GLYPH => {
|
BankType::GLYPH => {
|
||||||
if let Some((bank, _)) = self.gfx_policy.take_staging(handle_id) {
|
if let Some((bank, _)) = self.gfx_policy.take_staging(handle_id) {
|
||||||
@ -1131,6 +1368,11 @@ impl AssetManager {
|
|||||||
self.sound_policy.clear();
|
self.sound_policy.clear();
|
||||||
self.scene_policy.clear();
|
self.scene_policy.clear();
|
||||||
self.handles.write().unwrap().clear();
|
self.handles.write().unwrap().clear();
|
||||||
|
self.target_handles.write().unwrap().clear();
|
||||||
|
for generation in self.target_generations.write().unwrap().values_mut() {
|
||||||
|
*generation += 1;
|
||||||
|
}
|
||||||
|
self.load_worker.clear_pending();
|
||||||
self.pending_commits.lock().unwrap().clear();
|
self.pending_commits.lock().unwrap().clear();
|
||||||
self.gfx_slots.write().unwrap().fill(None);
|
self.gfx_slots.write().unwrap().fill(None);
|
||||||
self.glyph_slot_index.clear();
|
self.glyph_slot_index.clear();
|
||||||
@ -1526,6 +1768,80 @@ mod tests {
|
|||||||
assert!(Arc::ptr_eq(bank1, bank2));
|
assert!(Arc::ptr_eq(bank1, bank2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_asset_load_uses_stable_handle_for_target_slot() {
|
||||||
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
|
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
|
||||||
|
|
||||||
|
let data = test_glyph_asset_data();
|
||||||
|
let asset_entry = test_glyph_asset_entry("test_glyphs", 16, 16);
|
||||||
|
|
||||||
|
let am = AssetManager::new(
|
||||||
|
vec![asset_entry],
|
||||||
|
AssetsPayloadSource::from_bytes(data),
|
||||||
|
gfx_installer,
|
||||||
|
sound_installer,
|
||||||
|
scene_installer,
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_handle = am.load(0, 0).unwrap();
|
||||||
|
let second_handle = am.load(0, 0).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(first_handle, second_handle);
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
while am.status(second_handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||||
|
thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
assert_eq!(am.status(second_handle), LoadStatus::READY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_asset_load_supersedes_previous_request_for_target_slot() {
|
||||||
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
|
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
|
||||||
|
|
||||||
|
let first_payload = test_glyph_asset_data();
|
||||||
|
let second_payload = test_glyph_asset_data();
|
||||||
|
let second_offset = first_payload.len();
|
||||||
|
let mut data = first_payload;
|
||||||
|
data.extend_from_slice(&second_payload);
|
||||||
|
|
||||||
|
let first_asset = test_glyph_asset_entry("first_glyphs", 16, 16);
|
||||||
|
let mut second_asset = test_glyph_asset_entry("second_glyphs", 16, 16);
|
||||||
|
second_asset.asset_id = 1;
|
||||||
|
second_asset.offset = second_offset as u64;
|
||||||
|
|
||||||
|
let am = AssetManager::new(
|
||||||
|
vec![first_asset, second_asset],
|
||||||
|
AssetsPayloadSource::from_bytes(data),
|
||||||
|
gfx_installer,
|
||||||
|
sound_installer,
|
||||||
|
scene_installer,
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_handle = am.load(0, 0).unwrap();
|
||||||
|
let second_handle = am.load(1, 0).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(first_handle, second_handle);
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
while am.status(second_handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||||
|
thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
assert_eq!(am.status(second_handle), LoadStatus::READY);
|
||||||
|
|
||||||
|
assert_eq!(am.commit(second_handle), AssetOpStatus::Ok);
|
||||||
|
am.apply_commits();
|
||||||
|
|
||||||
|
let slot = am.slot_info(SlotRef::gfx(0));
|
||||||
|
assert_eq!(slot.asset_id, Some(1));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sound_asset_loading() {
|
fn test_sound_asset_loading() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
{"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":"in_progress","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-06-28","tags":["perf","asset","fs","async","scheduler","runtime"],"agendas":[{"id":"AGD-0008","file":"AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-06-28"}],"decisions":[{"id":"DEC-0034","file":"DEC-0034-async-work-lane-and-asset-backlog-contract.md","status":"accepted","created_at":"2026-06-28","updated_at":"2026-06-28","ref_agenda":"AGD-0008"}],"plans":[{"id":"PLN-0123","file":"PLN-0123-async-work-lane-specification-propagation.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0124","file":"PLN-0124-async-work-lane-runtime-infrastructure.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0125","file":"PLN-0125-asset-backlog-and-stable-slot-handles.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0126","file":"PLN-0126-asset-backlog-public-api-and-status-surface.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0127","file":"PLN-0127-async-lane-memcard-and-fs-integration-boundaries.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0128","file":"PLN-0128-async-asset-pipeline-contract-tests-and-telemetry.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]}],"lessons":[]}
|
{"type":"discussion","id":"DSC-0009","status":"in_progress","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-06-28","tags":["perf","asset","fs","async","scheduler","runtime"],"agendas":[{"id":"AGD-0008","file":"AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-06-28"}],"decisions":[{"id":"DEC-0034","file":"DEC-0034-async-work-lane-and-asset-backlog-contract.md","status":"accepted","created_at":"2026-06-28","updated_at":"2026-06-28","ref_agenda":"AGD-0008"}],"plans":[{"id":"PLN-0123","file":"PLN-0123-async-work-lane-specification-propagation.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0124","file":"PLN-0124-async-work-lane-runtime-infrastructure.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0125","file":"PLN-0125-asset-backlog-and-stable-slot-handles.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0126","file":"PLN-0126-asset-backlog-public-api-and-status-surface.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0127","file":"PLN-0127-async-lane-memcard-and-fs-integration-boundaries.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0128","file":"PLN-0128-async-asset-pipeline-contract-tests-and-telemetry.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]}],"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":"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-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"}]}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0125
|
id: PLN-0125
|
||||||
ticket: perf-async-background-work-lanes-for-assets-and-fs
|
ticket: perf-async-background-work-lanes-for-assets-and-fs
|
||||||
title: Asset Backlog and Stable Slot Handles
|
title: Asset Backlog and Stable Slot Handles
|
||||||
status: open
|
status: done
|
||||||
created: 2026-06-28
|
created: 2026-06-28
|
||||||
completed:
|
completed:
|
||||||
ref_decisions: [DEC-0034]
|
ref_decisions: [DEC-0034]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user