Skip to main content

u280_shell_rhdl/
scoreboard.rs

1//! Twelve-entry signer ownership scoreboard.
2
3use rhdl::prelude::*;
4
5use crate::abi::LIVE_JOB_SLOTS;
6
7/// Per-job ownership state.
8#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
9pub enum JobState {
10    /// Entry can be allocated.
11    #[default]
12    Empty,
13    /// The signer admitted the job but has not begun its data frame.
14    Active,
15    /// Beat zero was accepted and the noninterleaved frame is in progress.
16    Streaming,
17}
18
19/// One scoreboard slot.
20#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
21pub struct JobEntry {
22    /// Ownership phase.
23    pub state: JobState,
24    /// Opaque sequential job identity.
25    pub job_id: b32,
26    /// Physical signer cluster selected when the job was admitted.
27    pub cluster: b2,
28}
29
30/// Independent ownership transitions that may coincide for different jobs.
31#[allow(clippy::struct_excessive_bools)] // Preserve independent event-valid bits in the packed RHDL transition input.
32#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
33pub struct ScoreboardEvents {
34    /// Allocate one newly admitted job.
35    pub admit: bool,
36    /// Job named by `admit`.
37    pub admit_job: b32,
38    /// Cluster reported by the accepted signer admission.
39    pub admit_cluster: b2,
40    /// Move one active job to streaming on accepted beat zero.
41    pub frame_start: bool,
42    /// Job named by `frame_start`.
43    pub frame_start_job: b32,
44    /// Cluster carried by frame beat zero.
45    pub frame_start_cluster: b2,
46    /// Remove one streaming job after canonical beat 34 reaches BRAM.
47    pub frame_finish: bool,
48    /// Job named by `frame_finish`.
49    pub frame_finish_job: b32,
50    /// Remove one active job on an explicit signer error completion.
51    pub error: bool,
52    /// Job named by `error`.
53    pub error_job: b32,
54    /// Cluster carried by the signer error completion.
55    pub error_cluster: b2,
56}
57
58/// Updated table and fail-closed audit information.
59#[derive(Clone, Copy, Debug, Digital, Eq, PartialEq)]
60pub struct ScoreboardUpdate {
61    /// Next table contents.
62    pub entries: [JobEntry; LIVE_JOB_SLOTS],
63    /// Number of nonempty entries after all accepted transitions.
64    pub occupancy: b4,
65    /// A transition was missing, duplicated, conflicting, or in the wrong state.
66    pub fatal: bool,
67    /// First job associated with a failure in this update.
68    pub failing_job: b32,
69    /// Compact reason: 1 start, 2 finish, 3 error, 4 admission, 5 partition.
70    pub detail: b8,
71}
72
73impl Default for ScoreboardUpdate {
74    fn default() -> Self {
75        Self {
76            entries: [JobEntry::default(); LIVE_JOB_SLOTS],
77            occupancy: b4(0),
78            fatal: false,
79            failing_job: b32(0xffff_ffff),
80            detail: b8(0),
81        }
82    }
83}
84
85/// Map each exact signer-capacity slot to its physical four-context cluster.
86///
87/// Slots `0..=3`, `4..=7`, and `8..=11` belong to clusters zero, one, and two
88/// respectively.  Keeping this mapping explicit prevents a faulty fifth
89/// admission in one cluster from consuming spare capacity in another.
90#[kernel]
91fn scoreboard_slot_cluster_kernel(slot: b4) -> b2 {
92    if slot < b4(4) {
93        b2(0)
94    } else if slot < b4(8) {
95        b2(1)
96    } else {
97        b2(2)
98    }
99}
100
101/// Apply up to one transition of each kind, preserving exact job ownership.
102#[kernel]
103#[allow(clippy::assign_op_pattern, clippy::needless_range_loop)] // Preserve explicit ownership-count expressions used by the transition audit.
104pub fn scoreboard_update_kernel(
105    current: [JobEntry; LIVE_JOB_SLOTS],
106    events: ScoreboardEvents,
107) -> ScoreboardUpdate {
108    let mut next = current;
109    let mut fatal = false;
110    let mut failing_job = b32(0xffff_ffff);
111    let mut detail = b8(0);
112
113    // Audit every live entry before applying events.  A terminal event must
114    // not be allowed to erase pre-existing cross-partition corruption in the
115    // same cycle and make the table appear healthy again.
116    for index in 0..LIVE_JOB_SLOTS {
117        let expected_cluster = scoreboard_slot_cluster_kernel(b4(index as u128));
118        if current[index].state != JobState::Empty && current[index].cluster != expected_cluster {
119            if !fatal {
120                failing_job = current[index].job_id;
121                detail = b8(5);
122            }
123            fatal = true;
124        }
125    }
126
127    if events.frame_start && !fatal {
128        let mut matches = b4(0);
129        let mut exact_matches = b4(0);
130        let mut selected = b4(0);
131        for index in 0..LIVE_JOB_SLOTS {
132            if next[index].state != JobState::Empty && next[index].job_id == events.frame_start_job
133            {
134                matches = matches + b4(1);
135                let expected_cluster = scoreboard_slot_cluster_kernel(b4(index as u128));
136                if next[index].cluster == events.frame_start_cluster
137                    && next[index].cluster == expected_cluster
138                {
139                    exact_matches = exact_matches + b4(1);
140                    selected = b4(index as u128);
141                }
142            }
143        }
144        if matches != b4(1)
145            || exact_matches != b4(1)
146            || events.frame_start_cluster >= b2(3)
147            || next[selected].state != JobState::Active
148        {
149            fatal = true;
150            failing_job = events.frame_start_job;
151            detail = b8(1);
152        } else {
153            next[selected] = JobEntry {
154                state: JobState::Streaming,
155                job_id: events.frame_start_job,
156                cluster: events.frame_start_cluster,
157            };
158        }
159    }
160
161    if events.frame_finish && !fatal {
162        let mut matches = b4(0);
163        let mut exact_matches = b4(0);
164        let mut selected = b4(0);
165        for index in 0..LIVE_JOB_SLOTS {
166            if next[index].state != JobState::Empty && next[index].job_id == events.frame_finish_job
167            {
168                matches = matches + b4(1);
169                let expected_cluster = scoreboard_slot_cluster_kernel(b4(index as u128));
170                if next[index].cluster == expected_cluster {
171                    exact_matches = exact_matches + b4(1);
172                    selected = b4(index as u128);
173                }
174            }
175        }
176        if matches != b4(1) || exact_matches != b4(1) || next[selected].state != JobState::Streaming
177        {
178            if !fatal {
179                failing_job = events.frame_finish_job;
180                detail = b8(2);
181            }
182            fatal = true;
183        } else {
184            next[selected] = JobEntry::default();
185        }
186    }
187
188    if events.error && !fatal {
189        let mut matches = b4(0);
190        let mut exact_matches = b4(0);
191        let mut selected = b4(0);
192        for index in 0..LIVE_JOB_SLOTS {
193            if next[index].state != JobState::Empty && next[index].job_id == events.error_job {
194                matches = matches + b4(1);
195                let expected_cluster = scoreboard_slot_cluster_kernel(b4(index as u128));
196                if next[index].cluster == events.error_cluster
197                    && next[index].cluster == expected_cluster
198                {
199                    exact_matches = exact_matches + b4(1);
200                    selected = b4(index as u128);
201                }
202            }
203        }
204        if matches != b4(1)
205            || exact_matches != b4(1)
206            || events.error_cluster >= b2(3)
207            || next[selected].state != JobState::Active
208        {
209            if !fatal {
210                failing_job = events.error_job;
211                detail = b8(3);
212            }
213            fatal = true;
214        } else {
215            next[selected] = JobEntry::default();
216        }
217    }
218
219    if events.admit && !fatal {
220        let mut duplicates = b4(0);
221        let mut empty_found = false;
222        let mut empty_index = b4(0);
223        for index in 0..LIVE_JOB_SLOTS {
224            let expected_cluster = scoreboard_slot_cluster_kernel(b4(index as u128));
225            if next[index].state != JobState::Empty && next[index].job_id == events.admit_job {
226                duplicates = duplicates + b4(1);
227            }
228            if !empty_found
229                && expected_cluster == events.admit_cluster
230                && next[index].state == JobState::Empty
231            {
232                empty_found = true;
233                empty_index = b4(index as u128);
234            }
235        }
236        if duplicates != b4(0) || !empty_found || events.admit_cluster >= b2(3) {
237            if !fatal {
238                failing_job = events.admit_job;
239                detail = b8(4);
240            }
241            fatal = true;
242        } else {
243            next[empty_index] = JobEntry {
244                state: JobState::Active,
245                job_id: events.admit_job,
246                cluster: events.admit_cluster,
247            };
248        }
249    }
250
251    // Any failed combination is atomic from the scoreboard's perspective.
252    // The shell will enter its abort path, while the pre-event ownership table
253    // remains available for deterministic diagnostics and cleanup.
254    if fatal {
255        next = current;
256    }
257
258    let mut occupancy = b4(0);
259    for index in 0..LIVE_JOB_SLOTS {
260        if next[index].state != JobState::Empty {
261            occupancy = occupancy + b4(1);
262        }
263    }
264    ScoreboardUpdate {
265        entries: next,
266        occupancy,
267        fatal,
268        failing_job,
269        detail,
270    }
271}
272
273/// Count the currently occupied entries.
274#[kernel]
275#[allow(clippy::assign_op_pattern, clippy::needless_range_loop)] // Preserve the explicit occupancy-count expression used by the transition audit.
276pub fn scoreboard_occupancy_kernel(entries: [JobEntry; LIVE_JOB_SLOTS]) -> b4 {
277    let mut occupancy = b4(0);
278    for index in 0..LIVE_JOB_SLOTS {
279        if entries[index].state != JobState::Empty {
280            occupancy = occupancy + b4(1);
281        }
282    }
283    occupancy
284}