psx_residency_sim/
drive.rs1pub const MEASURED_SEEK_MILLIS: [(u32, u32); 4] = [(1, 11), (16, 79), (128, 137), (512, 310)];
6
7pub const READ_MICROS_PER_SECTOR_DOUBLE: u32 = 6_490;
9
10pub const READ_MICROS_PER_SECTOR_SINGLE: u32 = 13_200;
12
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
16pub enum Variance {
17 Nominal,
19 Pessimistic,
22 Seeded(u64),
25}
26
27#[derive(Clone, Debug)]
29pub struct DriveModel {
30 seek_millis: [(u32, u32); 4],
31 far_seek_millis: u32,
32 micros_per_sector: u32,
33 abort_micros: u32,
34 variance: Variance,
35 rng: u64,
36 head: Option<u32>,
37}
38
39impl DriveModel {
40 pub fn double_speed() -> Self {
43 Self {
44 seek_millis: MEASURED_SEEK_MILLIS,
45 far_seek_millis: MEASURED_SEEK_MILLIS[3].1,
46 micros_per_sector: READ_MICROS_PER_SECTOR_DOUBLE,
47 abort_micros: 0,
48 variance: Variance::Nominal,
49 rng: 0x9E37_79B9_7F4A_7C15,
50 head: None,
51 }
52 }
53
54 pub fn single_speed() -> Self {
57 Self {
58 micros_per_sector: READ_MICROS_PER_SECTOR_SINGLE,
59 ..Self::double_speed()
60 }
61 }
62
63 pub fn with_variance(mut self, variance: Variance) -> Self {
65 self.variance = variance;
66 if let Variance::Seeded(seed) = variance {
67 self.rng = seed | 1;
68 }
69 self
70 }
71
72 pub fn with_far_seek_millis(mut self, millis: u32) -> Self {
75 self.far_seek_millis = millis;
76 self
77 }
78
79 pub fn with_abort_micros(mut self, micros: u32) -> Self {
81 self.abort_micros = micros;
82 self
83 }
84
85 pub fn abort_micros(&self) -> u32 {
87 self.abort_micros
88 }
89
90 pub fn micros_per_sector(&self) -> u32 {
92 self.micros_per_sector
93 }
94
95 pub fn head(&self) -> Option<u32> {
97 self.head
98 }
99
100 pub fn lose_head(&mut self) {
102 self.head = None;
103 }
104
105 pub fn nominal_seek_micros(&self, distance: u32) -> u32 {
108 if distance == 0 {
109 return 0;
110 }
111 let table = &self.seek_millis;
112 if distance <= table[0].0 {
113 return table[0].1 * 1000;
114 }
115 for pair in table.windows(2) {
116 let (x0, y0) = (pair[0].0 as u64, pair[0].1 as u64 * 1000);
117 let (x1, y1) = (pair[1].0 as u64, pair[1].1 as u64 * 1000);
118 if distance as u64 <= x1 {
119 return (y0 + (y1 - y0) * (distance as u64 - x0) / (x1 - x0)) as u32;
120 }
121 }
122 self.far_seek_millis * 1000
123 }
124
125 fn next_factor_permille(&mut self) -> u64 {
126 match self.variance {
127 Variance::Nominal => 1000,
128 Variance::Pessimistic => 2000,
129 Variance::Seeded(_) => {
130 self.rng ^= self.rng << 13;
131 self.rng ^= self.rng >> 7;
132 self.rng ^= self.rng << 17;
133 1000 + (self.rng >> 11) % 1001
134 }
135 }
136 }
137
138 pub fn read(&mut self, lba: u32, sectors: u32) -> (u64, bool) {
142 let distance = match self.head {
143 Some(head) if head == lba => 0,
144 Some(head) => head.abs_diff(lba),
145 None => u32::MAX,
147 };
148 let seek = if distance == 0 {
149 0
150 } else {
151 let nominal = if distance == u32::MAX {
152 self.far_seek_millis as u64 * 1000
153 } else {
154 self.nominal_seek_micros(distance) as u64
155 };
156 nominal * self.next_factor_permille() / 1000
157 };
158 self.head = Some(lba + sectors);
159 (
160 seek + sectors as u64 * self.micros_per_sector as u64,
161 distance != 0,
162 )
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn table_points_are_reproduced_exactly() {
172 let drive = DriveModel::double_speed();
173 for (sectors, millis) in MEASURED_SEEK_MILLIS {
174 assert_eq!(drive.nominal_seek_micros(sectors), millis * 1000);
175 }
176 assert_eq!(drive.nominal_seek_micros(0), 0);
177 assert_eq!(drive.nominal_seek_micros(100_000), 310_000, "clamped");
178 }
179
180 #[test]
181 fn interpolation_is_monotonic_between_points() {
182 let drive = DriveModel::double_speed();
183 let mut last = 0;
184 for distance in 1..=600 {
185 let micros = drive.nominal_seek_micros(distance);
186 assert!(micros >= last, "seek time fell at {distance}");
187 last = micros;
188 }
189 assert_eq!(
190 drive.nominal_seek_micros(72),
191 79_000 + (137_000 - 79_000) * 56 / 112
192 );
193 }
194
195 #[test]
196 fn a_contiguous_read_needs_no_seek_and_costs_only_the_transfer() {
197 let mut drive = DriveModel::double_speed();
198 let (first, seeked) = drive.read(1000, 16);
199 assert!(seeked, "head position unknown at start");
200 assert_eq!(first, 310_000 + 16 * 6_490);
201 let (second, seeked) = drive.read(1016, 16);
202 assert!(!seeked);
203 assert_eq!(second, 16 * 6_490, "104 ms for a 16 sector region");
204 let (third, seeked) = drive.read(1016 + 16 + 128, 16);
205 assert!(seeked);
206 assert_eq!(third, 137_000 + 16 * 6_490);
207 }
208
209 #[test]
210 fn pessimistic_doubles_the_seek_only() {
211 let mut drive = DriveModel::double_speed().with_variance(Variance::Pessimistic);
212 drive.read(0, 1);
213 let (micros, _) = drive.read(1 + 128, 16);
214 assert_eq!(micros, 2 * 137_000 + 16 * 6_490);
215 }
216
217 #[test]
218 fn seeded_variance_is_deterministic_and_within_one_to_two_times() {
219 let run = |seed| {
220 let mut drive = DriveModel::double_speed().with_variance(Variance::Seeded(seed));
221 let mut out = alloc::vec::Vec::new();
222 drive.read(0, 1);
223 for step in 1..40u32 {
224 let (micros, _) = drive.read(step * 1000, 1);
225 out.push(micros - 6_490);
226 }
227 out
228 };
229 assert_eq!(run(5), run(5));
230 assert_ne!(run(5), run(6));
231 for seek in run(5) {
232 assert!((310_000..=620_000).contains(&seek), "{seek}");
233 }
234 }
235
236 #[test]
237 fn single_speed_reads_take_twice_as_long() {
238 let mut drive = DriveModel::single_speed();
239 drive.read(0, 1);
240 assert_eq!(drive.read(1, 10).0, 10 * 13_200);
241 }
242}