Skip to main content

rhdl_fpga_planner/
component.rs

1//! What a module tells the planner: the [`Component`] trait, its build
2//! options ([`Knob`]), its best build in a share ([`Plan`]) and the slot
3//! that holds it with its power ([`Slot`]).
4
5use crate::*;
6
7/// One build option of a component.
8#[derive(Clone, Debug, PartialEq)]
9pub struct Knob {
10    /// Label.
11    pub name: String,
12    /// Current value (for a choice: the index into `choices`).
13    pub value: f64,
14    /// Smallest value.
15    pub min: f64,
16    /// Largest value.
17    pub max: f64,
18    /// Step of one key press.
19    pub step: f64,
20    /// Names of the values of a choice knob (empty for a numeric knob).
21    pub choices: Vec<String>,
22    /// One setting for every module that has a shared knob of this name
23    /// (DSig's scheme and hash apply to its signer and its verifier alike):
24    /// the TUI shows it once and sets it on all of them.
25    pub shared: bool,
26}
27
28/// Constructors and stepping for knobs.
29impl Knob {
30    /// A numeric knob.
31    pub fn range(name: &str, value: f64, min: f64, max: f64, step: f64) -> Knob {
32        Knob { name: name.into(), value, min, max, step, choices: Vec::new(), shared: false }
33    }
34    /// A choice knob.
35    pub fn choice(name: &str, index: usize, choices: &[&str]) -> Knob {
36        Knob {
37            name: name.into(),
38            value: index as f64,
39            min: 0.0,
40            max: (choices.len() - 1) as f64,
41            step: 1.0,
42            choices: choices.iter().map(|c| c.to_string()).collect(),
43            shared: false,
44        }
45    }
46    /// The same knob, marked [shared](Knob::shared).
47    pub fn shared(mut self) -> Knob {
48        self.shared = true;
49        self
50    }
51    /// The value as text.
52    pub fn show(&self) -> String {
53        if self.choices.is_empty() { format!("{}", self.value) } else { self.choices[self.value as usize].clone() }
54    }
55    /// `value` moved by `steps` steps (choices wrap around).
56    pub fn moved(&self, steps: f64) -> f64 {
57        if self.choices.is_empty() {
58            (self.value + steps * self.step).clamp(self.min, self.max)
59        } else {
60            let n = self.choices.len() as i64;
61            (self.value as i64 + steps.signum() as i64).rem_euclid(n) as f64
62        }
63    }
64}
65
66/// A throughput a component needs from, or offers to, other components.
67#[derive(Clone, Debug, PartialEq)]
68pub struct Flow {
69    /// Service name, such as [`SHA512_BLOCKS`].
70    pub service: &'static str,
71    /// Units per second (for a demand: at the component's full capacity).
72    pub per_second: f64,
73}
74
75/// The best build of a component within its share.
76#[derive(Clone, Debug, PartialEq)]
77pub struct Plan {
78    /// Resources.
79    pub cost: Cost,
80    /// Capacity: operations per second at full use.
81    pub rate: f64,
82    /// What an operation is (`"sign"`).
83    pub unit: &'static str,
84    /// The resource that stops the next larger build.
85    pub limit: String,
86    /// One line describing the build.
87    pub detail: String,
88    /// Services this build consumes at full capacity.
89    pub demands: Vec<Flow>,
90    /// Services this build provides.
91    pub supplies: Vec<Flow>,
92    /// Costs come from Vivado (synthesis or route), not a model.
93    pub measured: bool,
94    /// Cycles from request to result with nothing else in flight.
95    pub latency_cycles: f64,
96    /// Requests in flight at full capacity (sets the loaded latency).
97    pub in_flight: f64,
98    /// DSig's batch size `K` for this build (DSig modules only; `None`
99    /// means [`DSIG_BATCH`]).
100    pub batch: Option<f64>,
101    /// Roles this build does itself, so the chip needs no separate module
102    /// for them (DSig with its own Ed25519 certificate core covers
103    /// [`Role::Ed25519Sign`]). Their need drops to zero.
104    pub covers: Vec<Role>,
105}
106
107/// Latency views of a plan.
108impl Plan {
109    /// Idle latency in microseconds at `mhz`.
110    pub fn idle_latency_us(&self, mhz: f64) -> f64 {
111        self.latency_cycles / mhz
112    }
113    /// Latency at full load in microseconds: by Little's law, requests in
114    /// flight divided by capacity, never below the idle latency.
115    pub fn loaded_latency_us(&self, mhz: f64) -> f64 {
116        let idle = self.idle_latency_us(mhz);
117        if self.rate <= 0.0 { idle } else { idle.max(1e6 * self.in_flight / self.rate) }
118    }
119}
120
121/// Compile-time settings for one component's build (written by the TUI).
122pub type BuildFlags = Vec<(String, String)>;
123
124/// A part of the chip the planner can size.
125pub trait Component {
126    /// Name in the planner.
127    fn name(&self) -> String;
128    /// What the component does.
129    fn role(&self) -> Role;
130    /// Build options.
131    fn knobs(&self) -> Vec<Knob>;
132    /// Set knob `index` to `value`.
133    fn set_knob(&mut self, index: usize, value: f64);
134    /// Best build within `share`, or `None` if nothing fits.
135    fn plan(&self, share: &Share, cx: &Context) -> Option<Plan>;
136    /// An allowance taken off the top of the budget before the powers divide
137    /// the rest (the SHA line's part of the LUT split), or `None` to take a
138    /// share of its side.
139    fn fixed(&self) -> Option<Share> {
140        None
141    }
142    /// The compile-time settings that build `plan` (constants, features),
143    /// as `key = value` pairs.
144    fn build_flags(&self, _plan: &Plan) -> BuildFlags {
145        Vec::new()
146    }
147    /// A line that sizes itself to demand ("auto"): the allowance of its
148    /// smallest build that supplies `per_second` units of what it supplies
149    /// (SHA-512 blocks) at `cx`'s clock, or `None` when its size is set by
150    /// hand (the default). The planner ([`plan_chip`]) calls it with the
151    /// demand of the chip it plans, so the line never limits the chip.
152    fn size_for(&self, _per_second: f64, _cx: &Context) -> Option<Share> {
153        None
154    }
155    /// Remember the allowance the planner settled on for an auto-sized line
156    /// ([`apply_layout`] calls it), so [`fixed`](Self::fixed) and the next
157    /// plan start from it.
158    fn resize(&mut self, _allowance: Share) {}
159    /// Why the current knob settings cannot be planned at all (an option
160    /// that is offered but not implemented yet), or `None`. The TUI shows
161    /// it instead of "nothing fits".
162    fn unavailable(&self) -> Option<String> {
163        None
164    }
165    /// A uBFT module: a service built to sit beside a uBFT replica (its
166    /// streams and key slots), or the replica's own footprint. Only the uBFT
167    /// modes plan it, and they plan nothing else ([`Mode::admits`]).
168    fn ubft(&self) -> bool {
169        false
170    }
171}
172
173/// A component with its share of the budget.
174pub struct Slot {
175    /// The component.
176    pub component: Box<dyn Component>,
177    /// Percentage of the budget this module gets (set by the side
178    /// balancing; ignored for fixed components).
179    pub power: f64,
180    /// Optional manual shares, as % of the budget's LUT, DSP, BRAM and URAM
181    /// ([`Resource`] order). A set limit replaces the power-derived share of
182    /// that resource and is a hard cap: the module never borrows more of
183    /// it. `None` (the default) leaves the resource to the powers and to
184    /// lending.
185    pub limits: [Option<f64>; 4],
186}
187
188/// A module of the chip with its power.
189impl Slot {
190    /// `component` with `power` % of the budget and no manual limits.
191    pub fn new(component: Box<dyn Component>, power: f64) -> Slot {
192        Slot { component, power, limits: [None; 4] }
193    }
194}