Skip to main content

rhdl_ed25519_fast_core/
parallel.rs

1//! Credit-bounded replication of the signing bottlenecks.
2//!
3//! Two engines may finish on the same cycle. Dropping either completion would
4//! silently corrupt the signer, so each lane has a local 31-entry BRAM FIFO.
5//! Credits cover the *entire* accepted-to-retired interval, including FIFO time.
6//! Thus a lane cannot produce more results than its queue can hold. Retirement
7//! is round-robin, one result per cycle, with caller tags unchanged.
8
9// RHDL kernels do not lower Rust let-chains; keep the nested hardware branch.
10#![allow(clippy::collapsible_if)]
11
12use crate::{
13    multicomb::{MultiCombMul, MultiCombMulIn, MultiCombMulOut},
14    point_compress::{PointCompressPair, PointCompressPairIn, PointCompressPairOut},
15    sha512_parallel::{Sha512Pool, Sha512PoolIn, Sha512PoolOut},
16};
17use rhdl::prelude::*;
18use rhdl_fpga::{
19    core::dff::DFF,
20    fifo::synchronous::{In as FifoIn, SyncFIFO},
21};
22
23#[derive(Copy, Clone, Debug, PartialEq, Digital)]
24#[doc(hidden)]
25pub struct MergeIn<T: Digital> {
26    pub first: Option<T>,
27    pub second: Option<T>,
28}
29
30#[derive(Copy, Clone, Debug, PartialEq, Digital)]
31#[doc(hidden)]
32pub struct MergeOut<T: Digital> {
33    pub value: Option<T>,
34    pub lane: bool,
35    pub overflow: bool,
36}
37
38/// Two independent BRAM completion queues and a fair one-port arbiter.
39#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
40pub struct Merge2<T: Digital> {
41    first: SyncFIFO<T, 5>,
42    second: SyncFIFO<T, 5>,
43    prefer_second: DFF<bool>,
44}
45
46impl<T: Digital> Default for Merge2<T> {
47    fn default() -> Self {
48        Self {
49            first: SyncFIFO::default(),
50            second: SyncFIFO::default(),
51            prefer_second: DFF::new(false),
52        }
53    }
54}
55
56impl<T: Digital> SynchronousIO for Merge2<T> {
57    type I = MergeIn<T>;
58    type O = MergeOut<T>;
59    type Kernel = merge_kernel<T>;
60}
61
62/// Completion merge transition. The producer must enforce 31 outstanding credits.
63#[kernel]
64pub fn merge_kernel<T: Digital>(
65    _cr: ClockReset,
66    i: MergeIn<T>,
67    q: Merge2Q<T>,
68) -> (MergeOut<T>, Merge2D<T>) {
69    let mut first = false;
70    let mut second = false;
71    let mut value = None;
72    if let Some(data) = q.first.data {
73        first = true;
74        value = Some(data);
75    }
76    if let Some(data) = q.second.data {
77        if !first || q.prefer_second {
78            first = false;
79            second = true;
80            value = Some(data);
81        }
82    }
83    let mut d = Merge2D::<T>::dont_care();
84    d.first = FifoIn::<T> {
85        data: i.first,
86        next: first,
87    };
88    d.second = FifoIn::<T> {
89        data: i.second,
90        next: second,
91    };
92    d.prefer_second = if first || second {
93        !second
94    } else {
95        q.prefer_second
96    };
97    (
98        MergeOut::<T> {
99            value,
100            lane: second,
101            overflow: q.first.overflow || q.second.overflow,
102        },
103        d,
104    )
105}
106
107#[derive(Copy, Clone, Debug, PartialEq, Digital)]
108#[doc(hidden)]
109pub struct Credits {
110    count: [b6; 2],
111    prefer_second: bool,
112}
113
114fn empty_credits() -> Credits {
115    Credits {
116        count: [b6(0); 2],
117        prefer_second: false,
118    }
119}
120
121#[kernel]
122fn select(credits: Credits, ready0: bool, ready1: bool) -> b2 {
123    let first = ready0 && credits.count[0] < b6(31);
124    let second = ready1 && credits.count[1] < b6(31);
125    if second && (!first || credits.prefer_second) {
126        b2(1)
127    } else if first {
128        b2(0)
129    } else {
130        b2(2)
131    }
132}
133
134#[kernel]
135fn update_credits(r: Credits, take: bool, lane: b2, retire: bool, retire_lane: bool) -> Credits {
136    let mut n = r;
137    if take {
138        n.count[lane] = n.count[lane] + b6(1);
139        n.prefer_second = lane == b2(0);
140    }
141    if retire {
142        let index = if retire_lane { b2(1) } else { b2(0) };
143        n.count[index] = n.count[index] - b6(1);
144    }
145    n
146}
147
148/// Two 24-context fixed-base engines, preserving the original request protocol.
149/// `occupancy` saturates at 31; it remains nonzero until queued results retire.
150#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
151pub struct DualMultiComb {
152    first: MultiCombMul,
153    second: MultiCombMul,
154    results: Merge2<MultiCombMulOut>,
155    credits: DFF<Credits>,
156}
157
158impl Default for DualMultiComb {
159    fn default() -> Self {
160        Self {
161            first: MultiCombMul::default(),
162            second: MultiCombMul::default(),
163            results: Merge2::default(),
164            credits: DFF::new(empty_credits()),
165        }
166    }
167}
168
169impl SynchronousIO for DualMultiComb {
170    type I = MultiCombMulIn;
171    type O = MultiCombMulOut;
172    type Kernel = dual_multicomb_kernel;
173}
174
175/// Fixed-base dispatch and retirement.
176#[kernel]
177pub fn dual_multicomb_kernel(
178    _cr: ClockReset,
179    i: MultiCombMulIn,
180    q: DualMultiCombQ,
181) -> (MultiCombMulOut, DualMultiCombD) {
182    let lane = select(q.credits, q.first.ready, q.second.ready);
183    let take = i.valid && lane != b2(2);
184    let mut o = q.first;
185    o.valid = false;
186    if let Some(value) = q.results.value {
187        o = value;
188        o.valid = true;
189    }
190    o.ready = lane != b2(2);
191    let occupied = q.credits.count[0] + q.credits.count[1];
192    o.occupancy = if occupied > b6(31) {
193        b5(31)
194    } else {
195        occupied.resize::<5>()
196    };
197    o.error = q.results.overflow;
198    let mut d = DualMultiCombD::dont_care();
199    d.first = i;
200    d.first.valid = take && lane == b2(0);
201    d.second = i;
202    d.second.valid = take && lane == b2(1);
203    d.results = MergeIn::<MultiCombMulOut> {
204        first: if q.first.valid { Some(q.first) } else { None },
205        second: if q.second.valid { Some(q.second) } else { None },
206    };
207    d.credits = update_credits(q.credits, take, lane, o.valid, q.results.lane);
208    (o, d)
209}
210
211/// Two pair-inversion engines, allowing four point encodings to progress in parallel.
212/// `occupancy` saturates at 15, including buffered completions.
213#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
214pub struct DualPointCompress {
215    first: PointCompressPair,
216    second: PointCompressPair,
217    results: Merge2<PointCompressPairOut>,
218    credits: DFF<Credits>,
219}
220
221impl Default for DualPointCompress {
222    fn default() -> Self {
223        Self {
224            first: PointCompressPair::default(),
225            second: PointCompressPair::default(),
226            results: Merge2::default(),
227            credits: DFF::new(empty_credits()),
228        }
229    }
230}
231
232impl SynchronousIO for DualPointCompress {
233    type I = PointCompressPairIn;
234    type O = PointCompressPairOut;
235    type Kernel = dual_compress_kernel;
236}
237
238/// Pair-compression dispatch and retirement.
239#[kernel]
240pub fn dual_compress_kernel(
241    _cr: ClockReset,
242    i: PointCompressPairIn,
243    q: DualPointCompressQ,
244) -> (PointCompressPairOut, DualPointCompressD) {
245    let lane = select(q.credits, q.first.ready, q.second.ready);
246    let take = i.valid && lane != b2(2);
247    let mut o = q.first;
248    o.valid = false;
249    if let Some(value) = q.results.value {
250        o = value;
251        o.valid = true;
252    }
253    o.ready = lane != b2(2);
254    let occupied = q.credits.count[0] + q.credits.count[1];
255    o.occupancy = if occupied > b6(15) {
256        b4(15)
257    } else {
258        occupied.resize::<4>()
259    };
260    o.error = q.results.overflow;
261    let mut d = DualPointCompressD::dont_care();
262    d.first = i;
263    d.first.valid = take && lane == b2(0);
264    d.second = i;
265    d.second.valid = take && lane == b2(1);
266    d.results = MergeIn::<PointCompressPairOut> {
267        first: if q.first.valid { Some(q.first) } else { None },
268        second: if q.second.valid { Some(q.second) } else { None },
269    };
270    d.credits = update_credits(q.credits, take, lane, o.valid, q.results.lane);
271    (o, d)
272}
273
274/// Eight SHA lanes with one issue port and one retirement port.
275///
276/// Only `request0` is supported (`request1_ready` is always false). Equal fixed
277/// lane latency and single issue guarantee at most one completion per cycle.
278#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
279pub struct Sha512Pool8 {
280    first: Sha512Pool,
281    second: Sha512Pool,
282    prefer_second: DFF<bool>,
283}
284
285impl Default for Sha512Pool8 {
286    fn default() -> Self {
287        Self {
288            first: Sha512Pool::default(),
289            second: Sha512Pool::default(),
290            prefer_second: DFF::new(false),
291        }
292    }
293}
294
295impl SynchronousIO for Sha512Pool8 {
296    type I = Sha512PoolIn;
297    type O = Sha512PoolOut;
298    type Kernel = sha_pool8_kernel;
299}
300
301/// Eight-lane single-issue SHA dispatch.
302#[kernel]
303pub fn sha_pool8_kernel(
304    _cr: ClockReset,
305    i: Sha512PoolIn,
306    q: Sha512Pool8Q,
307) -> (Sha512PoolOut, Sha512Pool8D) {
308    let second = q.second.request0_ready && (!q.first.request0_ready || q.prefer_second);
309    let ready = q.first.request0_ready || q.second.request0_ready;
310    let take = i.request0.valid && ready;
311    let mut d = Sha512Pool8D::dont_care();
312    d.first = i;
313    d.first.request0.valid = take && !second;
314    d.first.request1.valid = false;
315    d.second = i;
316    d.second.request0.valid = take && second;
317    d.second.request1.valid = false;
318    d.prefer_second = if take { !second } else { q.prefer_second };
319    let mut o = q.first;
320    if q.second.result0.valid {
321        o.result0 = q.second.result0;
322    }
323    o.result1.valid = false;
324    o.request0_ready = ready;
325    o.request1_ready = false;
326    (o, d)
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use crate::simulation::CycleSim;
333
334    #[test]
335    fn simultaneous_completions_are_lossless() {
336        let mut sim = CycleSim::new(Merge2::<b16>::default());
337        let mut seen = [false; 62];
338        for cycle in 0..130 {
339            let o = sim.tick(MergeIn {
340                first: (cycle < 31).then_some(b16(cycle)),
341                second: (cycle < 31).then_some(b16(cycle + 31)),
342            });
343            assert!(!o.overflow);
344            if let Some(value) = o.value {
345                let index = value.raw() as usize;
346                assert!(!seen[index], "duplicate {index}");
347                seen[index] = true;
348            }
349        }
350        assert!(seen.into_iter().all(|value| value));
351    }
352
353    #[test]
354    fn merge_and_sha_compile() -> miette::Result<()> {
355        rhdl::core::compile_design::<dual_multicomb_kernel>(
356            rhdl::core::CompilationMode::Synchronous,
357        )?;
358        rhdl::core::compile_design::<dual_compress_kernel>(
359            rhdl::core::CompilationMode::Synchronous,
360        )?;
361        Merge2::<b16>::default()
362            .descriptor(ScopedName::top())?
363            .hdl()?;
364        Sha512Pool8::default()
365            .descriptor(ScopedName::top())?
366            .hdl()?;
367        Ok(())
368    }
369
370    #[test]
371    fn eight_sha_lanes_keep_tags_and_results() {
372        use crate::sha512_parallel::Sha512PoolRequest;
373        let empty = Sha512PoolRequest {
374            valid: false,
375            tag: b9(0),
376            state: [b64(0); 8],
377            block: [b64(0); 16],
378        };
379        let mut sim = CycleSim::new(Sha512Pool8::default());
380        let mut o = sim.tick(Sha512PoolIn {
381            request0: empty,
382            request1: empty,
383        });
384        let mut accepted = 0;
385        let mut seen = [false; 16];
386        for _ in 0..600 {
387            let mut request = empty;
388            if o.request0_ready && accepted < 16 {
389                request.valid = true;
390                request.tag = b9(accepted);
391                accepted += 1;
392            }
393            o = sim.tick(Sha512PoolIn {
394                request0: request,
395                request1: empty,
396            });
397            assert!(!o.result1.valid);
398            if o.result0.valid {
399                let index = o.result0.tag.raw() as usize;
400                assert!(!seen[index]);
401                seen[index] = true;
402            }
403        }
404        assert_eq!(accepted, 16);
405        assert!(seen.into_iter().all(|value| value));
406    }
407}