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}