Skip to main content

sim_lib_topology/place/
model.rs

1use std::collections::BTreeMap;
2
3use sim_kernel::{Error, Export, Result, Symbol};
4use sim_lib_stream_core::{
5    BridgeLatency, ClockDomain, DomainBridgeDescriptor, DomainBridgeKind, LatencyClass,
6    RateContract,
7};
8
9use crate::{EdgeId, NodeId};
10
11/// Identifier for a placement site: a named host that nodes can be assigned to.
12#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct SiteId(Symbol);
14
15impl SiteId {
16    /// Builds a site id from a name.
17    pub fn new(name: impl Into<String>) -> Self {
18        Self(Symbol::new(name.into()))
19    }
20
21    /// Builds a site id from a runtime export symbol.
22    pub fn from_symbol(symbol: Symbol) -> Self {
23        Self(symbol)
24    }
25
26    /// Returns the underlying symbol.
27    pub fn as_symbol(&self) -> &Symbol {
28        &self.0
29    }
30}
31
32impl From<&str> for SiteId {
33    fn from(value: &str) -> Self {
34        Self::new(value)
35    }
36}
37
38impl From<String> for SiteId {
39    fn from(value: String) -> Self {
40        Self::new(value)
41    }
42}
43
44/// Capabilities a site offers: which latency classes it serves and whether it
45/// can host the audio (sample) clock.
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct SiteProfile {
48    id: SiteId,
49    export: Export,
50    latency_classes: Vec<LatencyClass>,
51    clock_domains: Vec<ClockDomain>,
52    audio_clock: bool,
53    stream_ports: bool,
54}
55
56impl SiteProfile {
57    /// Builds a site profile with an explicit latency-class set and clock flag.
58    pub fn new(
59        id: impl Into<SiteId>,
60        latency_classes: Vec<LatencyClass>,
61        audio_clock: bool,
62    ) -> Self {
63        let id = id.into();
64        let export = Export::Site {
65            symbol: id.as_symbol().clone(),
66            runtime_id: None,
67        };
68        Self {
69            id,
70            export,
71            clock_domains: default_clock_domains(audio_clock),
72            latency_classes,
73            audio_clock,
74            stream_ports: true,
75        }
76    }
77
78    /// Builds a placement profile from a kernel runtime site export and
79    /// topology site claims.
80    pub fn from_site_export(
81        export: Export,
82        latency_classes: Vec<LatencyClass>,
83        audio_clock: bool,
84    ) -> Result<Self> {
85        let Export::Site { symbol, .. } = &export else {
86            return Err(Error::Eval(
87                "topology placement site requires a kernel site export".to_owned(),
88            ));
89        };
90        Ok(Self {
91            id: SiteId::from_symbol(symbol.clone()),
92            export,
93            clock_domains: default_clock_domains(audio_clock),
94            latency_classes,
95            audio_clock,
96            stream_ports: true,
97        })
98    }
99
100    /// Preset for an audio-clock site that serves sample-exact through render
101    /// latency classes.
102    pub fn audio_clock(id: impl Into<SiteId>) -> Self {
103        Self::new(
104            id,
105            vec![
106                LatencyClass::SampleExact,
107                LatencyClass::BlockLocal,
108                LatencyClass::Interactive,
109                LatencyClass::OfflineRender,
110            ],
111            true,
112        )
113    }
114
115    /// Preset for a local worker site: block-local through render, no audio clock.
116    pub fn local_worker(id: impl Into<SiteId>) -> Self {
117        Self::new(
118            id,
119            vec![
120                LatencyClass::BlockLocal,
121                LatencyClass::Interactive,
122                LatencyClass::BufferedPreview,
123                LatencyClass::OfflineRender,
124            ],
125            false,
126        )
127    }
128
129    /// Preset for a buffered remote site: preview and collaboration latency
130    /// classes, no audio clock.
131    pub fn buffered_remote(id: impl Into<SiteId>) -> Self {
132        Self::new(
133            id,
134            vec![
135                LatencyClass::BufferedPreview,
136                LatencyClass::CollabBarDelay,
137                LatencyClass::RemoteCollaboration,
138                LatencyClass::OfflineRender,
139            ],
140            false,
141        )
142    }
143
144    /// Returns the site id.
145    pub fn id(&self) -> &SiteId {
146        &self.id
147    }
148
149    /// Returns the kernel site export this placement profile describes.
150    pub fn site_export(&self) -> &Export {
151        &self.export
152    }
153
154    /// Reports whether this site serves the given latency class.
155    pub fn supports_latency_class(&self, latency_class: LatencyClass) -> bool {
156        self.latency_classes.contains(&latency_class)
157    }
158
159    /// Sets the clock domains represented by this runtime site contract.
160    pub fn with_clock_domains(mut self, clock_domains: Vec<ClockDomain>) -> Self {
161        self.clock_domains = clock_domains;
162        self
163    }
164
165    /// Reports whether this site represents the given clock domain.
166    pub fn supports_clock_domain(&self, clock_domain: ClockDomain) -> bool {
167        self.clock_domains.contains(&clock_domain)
168    }
169
170    /// Sets whether this site can host stream-mode topology ports.
171    pub fn with_stream_ports(mut self, stream_ports: bool) -> Self {
172        self.stream_ports = stream_ports;
173        self
174    }
175
176    /// Reports whether this site can host stream-mode topology ports.
177    pub fn supports_stream_ports(&self) -> bool {
178        self.stream_ports
179    }
180
181    /// Reports whether this site can host the audio (sample) clock.
182    pub fn is_audio_clock(&self) -> bool {
183        self.audio_clock
184    }
185}
186
187/// Per-node placement requirements: rate contract, real-time pin, and the
188/// node's own latency contribution.
189#[derive(Clone, Debug, PartialEq, Eq)]
190pub struct PlacementNodeProfile {
191    rate_contract: RateContract,
192    realtime_pin: bool,
193    latency: BridgeLatency,
194}
195
196impl PlacementNodeProfile {
197    /// Builds a node profile from a rate contract and real-time pin flag.
198    pub fn new(rate_contract: RateContract, realtime_pin: bool) -> Self {
199        Self {
200            rate_contract,
201            realtime_pin,
202            latency: BridgeLatency::zero(),
203        }
204    }
205
206    /// Preset for a sample-exact node at an optional nominal rate.
207    pub fn sample_exact(nominal_rate_hz: Option<u32>, realtime_pin: bool) -> Self {
208        Self::new(RateContract::sample_exact(nominal_rate_hz), realtime_pin)
209    }
210
211    /// Preset for a block-local node.
212    pub fn block_local() -> Self {
213        Self::new(RateContract::block_local(), false)
214    }
215
216    /// Preset for a control-rate node.
217    pub fn control() -> Self {
218        Self::new(RateContract::control(), false)
219    }
220
221    /// Sets the node's own latency contribution, returning the updated profile.
222    pub fn with_latency(mut self, latency: BridgeLatency) -> Self {
223        self.latency = latency;
224        self
225    }
226
227    /// Returns the node's rate contract.
228    pub fn rate_contract(&self) -> RateContract {
229        self.rate_contract
230    }
231
232    /// Reports whether the node is pinned to a real-time clock.
233    pub fn realtime_pin(&self) -> bool {
234        self.realtime_pin
235    }
236
237    /// Returns the node's own latency contribution.
238    pub fn latency(&self) -> BridgeLatency {
239        self.latency
240    }
241}
242
243impl Default for PlacementNodeProfile {
244    fn default() -> Self {
245        Self::block_local()
246    }
247}
248
249/// Placement input: the known sites, per-node site assignments, and per-node
250/// profiles that `place` resolves against a graph.
251#[derive(Clone, Debug, PartialEq, Eq)]
252pub struct SiteMap {
253    default_site: SiteId,
254    sites: BTreeMap<SiteId, SiteProfile>,
255    assignments: BTreeMap<NodeId, SiteId>,
256    node_profiles: BTreeMap<NodeId, PlacementNodeProfile>,
257}
258
259impl SiteMap {
260    /// Builds a site map whose default (fallback) site is the given profile.
261    pub fn new(default_site: SiteProfile) -> Self {
262        let default_site_id = default_site.id().clone();
263        let mut sites = BTreeMap::new();
264        sites.insert(default_site_id.clone(), default_site);
265        Self {
266            default_site: default_site_id,
267            sites,
268            assignments: BTreeMap::new(),
269            node_profiles: BTreeMap::new(),
270        }
271    }
272
273    /// Registers another site, returning the updated map.
274    pub fn with_site(mut self, site: SiteProfile) -> Self {
275        self.sites.insert(site.id().clone(), site);
276        self
277    }
278
279    /// Assigns a node to a site, returning the updated map.
280    pub fn assign_node(mut self, node: impl Into<NodeId>, site: impl Into<SiteId>) -> Self {
281        self.assignments.insert(node.into(), site.into());
282        self
283    }
284
285    /// Sets a node's placement profile, returning the updated map.
286    pub fn with_node_profile(
287        mut self,
288        node: impl Into<NodeId>,
289        profile: PlacementNodeProfile,
290    ) -> Self {
291        self.node_profiles.insert(node.into(), profile);
292        self
293    }
294
295    /// Returns the site a node is assigned to, falling back to the default site.
296    pub fn site_for(&self, node: &NodeId) -> &SiteId {
297        self.assignments.get(node).unwrap_or(&self.default_site)
298    }
299
300    /// Returns a node's placement profile, defaulting to block-local.
301    pub fn profile_for(&self, node: &NodeId) -> PlacementNodeProfile {
302        self.node_profiles.get(node).cloned().unwrap_or_default()
303    }
304
305    /// Looks up a registered site profile by id.
306    pub fn site_profile(&self, site: &SiteId) -> Option<&SiteProfile> {
307        self.sites.get(site)
308    }
309}
310
311/// Outcome of placing a graph: where nodes landed, the clock-domain bridges
312/// inserted across edges, the resulting latency budget, and any refusals.
313#[derive(Clone, Debug, PartialEq, Eq)]
314pub struct PlacementReport {
315    /// The site and clock assignment computed for each node.
316    pub placed: Vec<PlacedNode>,
317    /// Bridges inserted on edges that cross a clock domain or site boundary.
318    pub bridges: Vec<DomainBridge>,
319    /// Accumulated latency reaching each output node.
320    pub latency: Vec<PortLatency>,
321    /// Placements that could not be satisfied.
322    pub refusals: Vec<PlacementRefusal>,
323}
324
325impl PlacementReport {
326    /// Reports whether placement succeeded with no refusals.
327    pub fn is_accepted(&self) -> bool {
328        self.refusals.is_empty()
329    }
330}
331
332/// A node's resolved placement: its site, clock domain, and latency class.
333#[derive(Clone, Debug, PartialEq, Eq)]
334pub struct PlacedNode {
335    /// The placed node.
336    pub node: NodeId,
337    /// The site the node was assigned to.
338    pub site: SiteId,
339    /// The node's resolved clock domain.
340    pub clock_domain: ClockDomain,
341    /// The node's resolved latency class.
342    pub latency_class: LatencyClass,
343    /// Whether the node is pinned to a real-time clock.
344    pub realtime_pin: bool,
345}
346
347/// A clock-domain bridge inserted on an edge that crosses domains or sites.
348#[derive(Clone, Debug, PartialEq, Eq)]
349pub struct DomainBridge {
350    /// The bridged edge.
351    pub edge: EdgeId,
352    /// The source node.
353    pub from: NodeId,
354    /// The destination node.
355    pub to: NodeId,
356    /// The source node's site.
357    pub from_site: SiteId,
358    /// The destination node's site.
359    pub to_site: SiteId,
360    /// The bridge descriptor (resampler, gate, jitter buffer, ...).
361    pub descriptor: DomainBridgeDescriptor,
362}
363
364impl DomainBridge {
365    /// Returns the bridge kind from its descriptor.
366    pub fn kind(&self) -> DomainBridgeKind {
367        self.descriptor.kind()
368    }
369}
370
371/// Accumulated latency reaching one output node under a placement.
372#[derive(Clone, Debug, PartialEq, Eq)]
373pub struct PortLatency {
374    /// The output node.
375    pub node: NodeId,
376    /// The node's site.
377    pub site: SiteId,
378    /// The accumulated latency along the worst path reaching the node.
379    pub latency: BridgeLatency,
380    /// The node's latency class.
381    pub latency_class: LatencyClass,
382}
383
384/// A placement that could not be satisfied at the assigned site.
385#[derive(Clone, Debug, PartialEq, Eq)]
386pub struct PlacementRefusal {
387    /// The refused node.
388    pub node: NodeId,
389    /// The site the node was assigned to.
390    pub site: SiteId,
391    /// Why the placement was refused.
392    pub reason: PlacementRefusalReason,
393}
394
395/// Why a node could not be placed at its assigned site.
396#[derive(Clone, Debug, PartialEq, Eq)]
397pub enum PlacementRefusalReason {
398    /// The assigned site is not registered in the site map.
399    UnknownSite,
400    /// A real-time-pinned node was assigned to a site without an audio clock.
401    RealtimePinViolation,
402    /// The site does not serve the node's latency class.
403    UnsupportedLatencyClass,
404    /// The site does not represent the node's clock domain.
405    UnsupportedClockDomain {
406        /// The requested clock domain.
407        domain: ClockDomain,
408    },
409    /// The site does not claim support for stream-mode topology ports.
410    UnsupportedStreamPorts,
411    /// The edge crosses clock domains that have no semantic bridge.
412    IncomparableClockDomain {
413        /// The source node's clock domain.
414        from: ClockDomain,
415        /// The destination node's clock domain.
416        to: ClockDomain,
417    },
418}
419
420fn default_clock_domains(audio_clock: bool) -> Vec<ClockDomain> {
421    let mut domains = vec![
422        ClockDomain::Block,
423        ClockDomain::Control,
424        ClockDomain::MidiTick,
425        ClockDomain::Wall,
426        ClockDomain::Job,
427    ];
428    if audio_clock {
429        domains.insert(0, ClockDomain::Sample);
430    }
431    domains
432}