Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_residency/
lib.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! Residency policy for streamed data.
3//!
4//! A game that streams its world needs the same few decisions again and again:
5//! which resources must be in RAM, VRAM or SPU RAM now, which to read next,
6//! what to throw out to make room, and what to do when a read fails. This
7//! crate makes those decisions and nothing else. It never touches the drive:
8//! requests go out through the [`Host`] trait and outcomes come back through
9//! [`Residency::complete_read`], so the same policy runs against the
10//! interrupt-driven transport on the console and against the drive model in
11//! `psx-residency-sim` on the host.
12//!
13//! # Vocabulary
14//!
15//! * A [`ResourceKey`] names one streamed resource: a [`Category`] (world
16//!   region, collision region, texture page, archetype pack, audio bank, or a
17//!   game-defined one) and the engine's dense index inside it.
18//! * Each category has a [`CategoryConfig`] budget (pages, entries) and
19//!   allocates from a page pool. A [`PagePool`] hands out contiguous runs of
20//!   pages and a [`Handle`] that stops resolving the moment its run is freed
21//!   or reused; [`PageBuffer`] is the RAM behind a RAM pool.
22//! * Every frame the game passes the [`Wanted`] list to [`Residency::step`]:
23//!   key, [`Class`] (demand, combat fill, lead, background, audio), walk
24//!   distance and an optional deadline. Pinned keys ([`Residency::pin`]) are
25//!   wanted too, at their pin class.
26//!
27//! # The frame loop
28//!
29//! 1. `step(&wanted, &mut host, &mut evictions)`. Misses are ordered by class,
30//!    deadline and distance and issued to the host until the transport is at
31//!    depth. A request that arrives while the same resource is already in
32//!    flight or landed is adopted, not repeated, and a more urgent class
33//!    promotes the request in flight. Room comes from free pages, then from
34//!    sliding movable runs down (compaction), then from evicting the cheapest
35//!    contiguous window.
36//! 2. Apply the returned evictions: unlink the resource from whatever points
37//!    at it. Their handles are already stale.
38//! 3. The transport reports each request with [`Residency::complete_read`].
39//!    A category that installs lands the payload as [`State::Landed`]; the
40//!    game calls [`Residency::begin_install`], does its verify, relocate and
41//!    upload work in slices, then [`Residency::finish_install`] (or
42//!    [`Residency::fail_install`]). Only then is the resource resident.
43//! 4. Read resident bytes through `PageBuffer::bytes(residency.try_run(handle)?, len)`.
44//!    A handle outlives its data only by turning stale, never by pointing at
45//!    someone else's.
46//!
47//! # Eviction
48//!
49//! A victim is never pinned, never reading or installing, and never wanted at
50//! a class [`Config::protect_wanted`] protects. Among the rest, in order:
51//! tier 0 (unwanted, beyond the category's keep radius), tier 1 (unwanted,
52//! inside the hysteresis band), tier 2 (wanted, but lower priority than the
53//! requester by class, or by more than [`Config::displace_margin`] of distance
54//! inside a class). Each requester class has a ceiling tier it may reach
55//! ([`Config::max_victim_tier`]): a background fill takes only tier 0, a lead
56//! request reaches tier 1, a demand tier 2. Inside a tier the farthest goes
57//! first, then the least recently used. Because a run must be contiguous, the
58//! engine picks the cheapest window of adjacent evictable runs (lowest worst
59//! tier, then fewest victims, then farthest, then oldest), not just the best
60//! single victim. Eviction commits inside [`Residency::step`], at the frame
61//! boundary, and stops rather than evict what it cannot report.
62//!
63//! # Budgets and CPU
64//!
65//! Per-category page and entry budgets are enforced before anything is
66//! allocated: a category over budget evicts its own entries, never another
67//! category's. A step also has a bookkeeping budget ([`Config::step_budget`])
68//! counted in work units: one per wanted entry examined, one per table entry
69//! or pool run the eviction search visits, one per sort swap, and
70//! `1 + pages / 8` per compaction move. Demand and combat-fill entries are
71//! never deferred; the rest stop when the budget runs out and are picked up
72//! next window (the wanted list is stateless). The unit is relative; convert
73//! it to cycles by measuring on the target.
74//!
75//! # Failure
76//!
77//! A failed read or install frees its pages and puts the key in backoff
78//! ([`Backoff`]): 16 windows after the first failure, doubling to 512. The
79//! key is never abandoned. A cancelled request keeps its pages reserved until
80//! the transport reports it finished, because sectors may still be landing.
81//!
82//! # Other modules
83//!
84//! [`slots`] is the fixed-slot policy lifted from hk-psx's `room_residency.rs`
85//! (private arenas, a wishlist, a stored-then-decoded pipeline) for games that
86//! keep a handful of equally sized slots. `psx-residency-sim`, a separate
87//! host-only crate, models the drive from the silicon seek and read
88//! measurements and replays routes against the real engine to report deadline
89//! slack and misses.
90//!
91//! Unlike `psx-cache`, which caches small values in fixed slots, this crate
92//! owns variable-length contiguous page runs and the request lifecycle.
93//!
94//! ```
95//! use psx_residency::*;
96//!
97//! struct Disc;
98//! impl Host for Disc {
99//!     fn extent(&self, key: ResourceKey) -> Option<Extent> {
100//!         Some(Extent { lba: 100 * key.index(), sectors: 4, pages: 4, bytes: 8000 })
101//!     }
102//!     fn walk_distance(&self, _: ResourceKey) -> u32 { 0 }
103//!     fn submit(&mut self, _: &ReadRequest) -> Submit { Submit::Accepted }
104//!     fn cancel(&mut self, _: RequestId) {}
105//! }
106//!
107//! let mut config = Config::<2>::new();
108//! config.pools[0] = PoolConfig { capacity_pages: 16, placement: Placement::BestFit };
109//! config.categories[0] = CategoryConfig {
110//!     pool: 0, max_pages: 16, max_entries: 4, movable: true, installs: false, keep_radius: 300,
111//! };
112//! let mut residency = Residency::<8, 4, 2>::new(config);
113//! let region = ResourceKey::new(Category::WORLD_REGION, 7).unwrap();
114//! let mut evictions = [];
115//!
116//! let report = residency.step(&[Wanted::new(region, Class::Demand, 0)], &mut Disc, &mut evictions);
117//! assert_eq!(report.stats.issued, 1);
118//! let request = residency.request_of(region).unwrap();
119//! residency.complete_read(request, ReadOutcome::Done);
120//! assert!(residency.is_resident(region));
121//! ```
122#![no_std]
123
124#[cfg(test)]
125extern crate std;
126
127mod backoff;
128mod buffer;
129mod config;
130mod engine;
131mod host;
132mod key;
133mod pool;
134pub mod slots;
135#[cfg(test)]
136mod tests;
137
138pub use backoff::Backoff;
139pub use buffer::PageBuffer;
140pub use config::{CategoryConfig, Config, PoolConfig};
141pub use engine::{Residency, State, StepReport, Totals, Wanted, WindowStats};
142pub use host::{Eviction, Extent, Host, ReadOutcome, ReadRequest, RequestId, Submit};
143pub use key::{Category, Class, ResourceKey};
144pub use pool::{Fragmentation, Handle, Moved, PagePool, PageRun, Placement, PAGE_BYTES};