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

psx_residency/
key.rs

1//! Resource keys, categories and request priority classes.
2
3/// What kind of data a resource is. Each category has its own budget
4/// ([`crate::CategoryConfig`]) and maps onto one page pool.
5///
6/// The five constants are the categories the streaming design names; a game
7/// may use any other value below [`Category::LIMIT`] for its own kinds.
8#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct Category(u8);
10
11impl Category {
12    /// Exclusive upper bound on category numbers (a key packs the category in
13    /// 6 bits).
14    pub const LIMIT: u8 = 64;
15    /// Visible world data of one region (a BSP subtree and its payload).
16    pub const WORLD_REGION: Category = Category(0);
17    /// Collision data of one region (hull subtrees).
18    pub const COLLISION_REGION: Category = Category(1);
19    /// One VRAM texture page or palette block.
20    pub const TEXTURE_PAGE: Category = Category(2);
21    /// Model, animation clips and atlas of one actor archetype.
22    pub const ARCHETYPE_PACK: Category = Category(3);
23    /// One SPU sound bank.
24    pub const AUDIO_BANK: Category = Category(4);
25
26    /// A game-defined category. `None` when `number` is not below
27    /// [`Category::LIMIT`].
28    pub const fn from_number(number: u8) -> Option<Category> {
29        if number < Self::LIMIT {
30            Some(Category(number))
31        } else {
32            None
33        }
34    }
35
36    /// The category number, usable as an array index.
37    pub const fn number(self) -> u8 {
38        self.0
39    }
40}
41
42/// Identity of one streamed resource: a [`Category`] and the engine's dense
43/// index inside it (region number, texture page number, archetype number).
44///
45/// Packed into one `u32` so a lookup is one integer compare.
46#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub struct ResourceKey(u32);
48
49impl ResourceKey {
50    /// Largest index a key can carry (24 bits).
51    pub const MAX_INDEX: u32 = (1 << 24) - 1;
52
53    /// A key for `index` inside `category`. `None` when `index` exceeds
54    /// [`ResourceKey::MAX_INDEX`].
55    pub const fn new(category: Category, index: u32) -> Option<ResourceKey> {
56        if index > Self::MAX_INDEX {
57            None
58        } else {
59            Some(ResourceKey(((category.0 as u32) << 24) | index))
60        }
61    }
62
63    /// The key's category.
64    pub const fn category(self) -> Category {
65        Category((self.0 >> 24) as u8)
66    }
67
68    /// The key's index inside its category.
69    pub const fn index(self) -> u32 {
70        self.0 & Self::MAX_INDEX
71    }
72
73    /// The packed representation, stable across builds.
74    pub const fn as_u32(self) -> u32 {
75        self.0
76    }
77
78    /// Rebuild a key from [`ResourceKey::as_u32`]. `None` when the category
79    /// bits are out of range.
80    pub const fn from_u32(packed: u32) -> Option<ResourceKey> {
81        if (packed >> 24) < Category::LIMIT as u32 {
82            Some(ResourceKey(packed))
83        } else {
84            None
85        }
86    }
87}
88
89/// Request priority, most urgent first. The order is the order requests are
90/// issued to the transport and the order eviction defends them.
91#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
92#[repr(u8)]
93pub enum Class {
94    /// Needed by the current frame (invariant: always resident). Never
95    /// deferred by the CPU budget and never evicted while wanted.
96    Demand = 0,
97    /// Fill for the combat scope: must complete before combat music starts.
98    CombatFill = 1,
99    /// Lead ring: needed soon, requested nearest first.
100    Lead = 2,
101    /// Opportunistic fill of whatever room is left.
102    Background = 3,
103    /// Audio banks and ring refills.
104    Audio = 4,
105}
106
107impl Class {
108    /// Number of classes.
109    pub const COUNT: usize = 5;
110
111    /// The class as an array index.
112    pub const fn as_index(self) -> usize {
113        self as usize
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn key_round_trips_and_rejects_out_of_range() {
123        let key = ResourceKey::new(Category::ARCHETYPE_PACK, 0xABCDE).unwrap();
124        assert_eq!(key.category(), Category::ARCHETYPE_PACK);
125        assert_eq!(key.index(), 0xABCDE);
126        assert_eq!(ResourceKey::from_u32(key.as_u32()), Some(key));
127        assert!(ResourceKey::new(Category::WORLD_REGION, ResourceKey::MAX_INDEX + 1).is_none());
128        assert!(ResourceKey::from_u32(0xFF00_0000).is_none());
129        assert!(Category::from_number(Category::LIMIT).is_none());
130    }
131
132    #[test]
133    fn classes_order_demand_first() {
134        assert!(Class::Demand < Class::CombatFill);
135        assert!(Class::Lead < Class::Background);
136        assert_eq!(Class::Audio.as_index(), Class::COUNT - 1);
137    }
138}