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}