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

psx_residency_sim/
drive.rs

1//! The CD drive as the silicon measurements describe it.
2
3/// Seek time by distance, measured on silicon at double speed: sectors and
4/// milliseconds. Survey table; the swing between runs is about 2x.
5pub const MEASURED_SEEK_MILLIS: [(u32, u32); 4] = [(1, 11), (16, 79), (128, 137), (512, 310)];
6
7/// Microseconds per sector at double speed (6.49 ms), measured on silicon.
8pub const READ_MICROS_PER_SECTOR_DOUBLE: u32 = 6_490;
9
10/// Microseconds per sector at single speed (13.20 ms), measured on silicon.
11pub const READ_MICROS_PER_SECTOR_SINGLE: u32 = 13_200;
12
13/// How the run-to-run swing of seek time is modelled. The measured table is
14/// one run; the survey reports the true value swinging by about 2x.
15#[derive(Copy, Clone, Debug, PartialEq, Eq)]
16pub enum Variance {
17    /// Seeks take the table value.
18    Nominal,
19    /// Every seek takes twice the table value: the design's pessimistic corner
20    /// ("the swing applies to the seek only").
21    Pessimistic,
22    /// Each seek takes the table value times a factor drawn uniformly from
23    /// 1.0 to 2.0 by a deterministic generator seeded with this value.
24    Seeded(u64),
25}
26
27/// A drive: where the head is, how long a read takes, how long a seek takes.
28#[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    /// Double speed, nominal seeks, the measured table, clamped beyond 512
41    /// sectors, instant abort.
42    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    /// The same at single speed. The seek table was measured at double speed
55    /// and is kept as is (an assumption).
56    pub fn single_speed() -> Self {
57        Self {
58            micros_per_sector: READ_MICROS_PER_SECTOR_SINGLE,
59            ..Self::double_speed()
60        }
61    }
62
63    /// Choose how seek time varies between runs.
64    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    /// Seek time for any distance beyond the measured 512 sectors. Unmeasured:
73    /// the default clamps to the 512-sector value.
74    pub fn with_far_seek_millis(mut self, millis: u32) -> Self {
75        self.far_seek_millis = millis;
76        self
77    }
78
79    /// Time a cancelled read takes to stop (default 0: unmeasured).
80    pub fn with_abort_micros(mut self, micros: u32) -> Self {
81        self.abort_micros = micros;
82        self
83    }
84
85    /// Time a cancelled read takes to stop.
86    pub fn abort_micros(&self) -> u32 {
87        self.abort_micros
88    }
89
90    /// Microseconds to read one sector.
91    pub fn micros_per_sector(&self) -> u32 {
92        self.micros_per_sector
93    }
94
95    /// The sector under the head, when known.
96    pub fn head(&self) -> Option<u32> {
97        self.head
98    }
99
100    /// Forget the head position (after an abort or a stop).
101    pub fn lose_head(&mut self) {
102        self.head = None;
103    }
104
105    /// The table seek time in microseconds for a jump of `distance` sectors,
106    /// before variance. Linear between measured points (an assumption).
107    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    /// Time to read `sectors` sectors starting at `lba`, moving the head. A
139    /// read that starts where the head already is needs no seek (the
140    /// transport chains contiguous requests). Returns `(micros, seeked)`.
141    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            // Unknown position: charge the far seek.
146            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}