use std::collections::BTreeMap;
use sim_kernel::{Cx, Result, Symbol};
use sim_lib_stream_core::{
BridgeLatency, ClockDomain, DomainBridgeDescriptor, DomainBridgeKind, LatencyClass,
RateContract,
};
use crate::{CompiledGraph, EdgeId, Graph, NodeId, compile_graph};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SiteId(Symbol);
impl SiteId {
pub fn new(name: impl Into<String>) -> Self {
Self(Symbol::new(name.into()))
}
pub fn as_symbol(&self) -> &Symbol {
&self.0
}
}
impl From<&str> for SiteId {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for SiteId {
fn from(value: String) -> Self {
Self::new(value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SiteProfile {
id: SiteId,
latency_classes: Vec<LatencyClass>,
audio_clock: bool,
}
impl SiteProfile {
pub fn new(
id: impl Into<SiteId>,
latency_classes: Vec<LatencyClass>,
audio_clock: bool,
) -> Self {
Self {
id: id.into(),
latency_classes,
audio_clock,
}
}
pub fn audio_clock(id: impl Into<SiteId>) -> Self {
Self::new(
id,
vec![
LatencyClass::SampleExact,
LatencyClass::BlockLocal,
LatencyClass::Interactive,
LatencyClass::OfflineRender,
],
true,
)
}
pub fn local_worker(id: impl Into<SiteId>) -> Self {
Self::new(
id,
vec![
LatencyClass::BlockLocal,
LatencyClass::Interactive,
LatencyClass::BufferedPreview,
LatencyClass::OfflineRender,
],
false,
)
}
pub fn buffered_remote(id: impl Into<SiteId>) -> Self {
Self::new(
id,
vec![
LatencyClass::BufferedPreview,
LatencyClass::CollabBarDelay,
LatencyClass::RemoteCollaboration,
LatencyClass::OfflineRender,
],
false,
)
}
pub fn id(&self) -> &SiteId {
&self.id
}
pub fn supports_latency_class(&self, latency_class: LatencyClass) -> bool {
self.latency_classes.contains(&latency_class)
}
pub fn is_audio_clock(&self) -> bool {
self.audio_clock
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PlacementNodeProfile {
rate_contract: RateContract,
realtime_pin: bool,
latency: BridgeLatency,
}
impl PlacementNodeProfile {
pub fn new(rate_contract: RateContract, realtime_pin: bool) -> Self {
Self {
rate_contract,
realtime_pin,
latency: BridgeLatency::zero(),
}
}
pub fn sample_exact(nominal_rate_hz: Option<u32>, realtime_pin: bool) -> Self {
Self::new(RateContract::sample_exact(nominal_rate_hz), realtime_pin)
}
pub fn block_local() -> Self {
Self::new(RateContract::block_local(), false)
}
pub fn control() -> Self {
Self::new(RateContract::control(), false)
}
pub fn with_latency(mut self, latency: BridgeLatency) -> Self {
self.latency = latency;
self
}
pub fn rate_contract(&self) -> RateContract {
self.rate_contract
}
pub fn realtime_pin(&self) -> bool {
self.realtime_pin
}
pub fn latency(&self) -> BridgeLatency {
self.latency
}
}
impl Default for PlacementNodeProfile {
fn default() -> Self {
Self::block_local()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SiteMap {
default_site: SiteId,
sites: BTreeMap<SiteId, SiteProfile>,
assignments: BTreeMap<NodeId, SiteId>,
node_profiles: BTreeMap<NodeId, PlacementNodeProfile>,
}
impl SiteMap {
pub fn new(default_site: SiteProfile) -> Self {
let default_site_id = default_site.id().clone();
let mut sites = BTreeMap::new();
sites.insert(default_site_id.clone(), default_site);
Self {
default_site: default_site_id,
sites,
assignments: BTreeMap::new(),
node_profiles: BTreeMap::new(),
}
}
pub fn with_site(mut self, site: SiteProfile) -> Self {
self.sites.insert(site.id().clone(), site);
self
}
pub fn assign_node(mut self, node: impl Into<NodeId>, site: impl Into<SiteId>) -> Self {
self.assignments.insert(node.into(), site.into());
self
}
pub fn with_node_profile(
mut self,
node: impl Into<NodeId>,
profile: PlacementNodeProfile,
) -> Self {
self.node_profiles.insert(node.into(), profile);
self
}
pub fn site_for(&self, node: &NodeId) -> &SiteId {
self.assignments.get(node).unwrap_or(&self.default_site)
}
pub fn profile_for(&self, node: &NodeId) -> PlacementNodeProfile {
self.node_profiles.get(node).cloned().unwrap_or_default()
}
pub fn site_profile(&self, site: &SiteId) -> Option<&SiteProfile> {
self.sites.get(site)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PlacementReport {
pub placed: Vec<PlacedNode>,
pub bridges: Vec<DomainBridge>,
pub latency: Vec<PortLatency>,
pub refusals: Vec<PlacementRefusal>,
}
impl PlacementReport {
pub fn is_accepted(&self) -> bool {
self.refusals.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PlacedNode {
pub node: NodeId,
pub site: SiteId,
pub clock_domain: ClockDomain,
pub latency_class: LatencyClass,
pub realtime_pin: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DomainBridge {
pub edge: EdgeId,
pub from: NodeId,
pub to: NodeId,
pub from_site: SiteId,
pub to_site: SiteId,
pub descriptor: DomainBridgeDescriptor,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PortLatency {
pub node: NodeId,
pub site: SiteId,
pub latency: BridgeLatency,
pub latency_class: LatencyClass,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PlacementRefusal {
pub node: NodeId,
pub site: SiteId,
pub reason: PlacementRefusalReason,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PlacementRefusalReason {
UnknownSite,
RealtimePinViolation,
UnsupportedLatencyClass,
}
pub fn place(cx: &mut Cx, topology: &Graph, sites: &SiteMap) -> Result<PlacementReport> {
let compiled = compile_graph(cx, topology)?;
Ok(place_graph(&compiled, sites))
}
pub fn place_graph(graph: &CompiledGraph, sites: &SiteMap) -> PlacementReport {
let placed = placed_nodes(graph, sites);
let refusals = placement_refusals(graph, sites);
let bridges = domain_bridges(graph, sites);
let latency = latency_budget(graph, sites, &bridges);
PlacementReport {
placed,
bridges,
latency,
refusals,
}
}
fn placed_nodes(graph: &CompiledGraph, sites: &SiteMap) -> Vec<PlacedNode> {
graph
.nodes
.iter()
.map(|node| {
let profile = sites.profile_for(&node.id);
PlacedNode {
node: node.id.clone(),
site: sites.site_for(&node.id).clone(),
clock_domain: profile.rate_contract().clock_domain(),
latency_class: profile.rate_contract().latency_class(),
realtime_pin: profile.realtime_pin(),
}
})
.collect()
}
fn placement_refusals(graph: &CompiledGraph, sites: &SiteMap) -> Vec<PlacementRefusal> {
let mut refusals = Vec::new();
for node in &graph.nodes {
let site_id = sites.site_for(&node.id).clone();
let profile = sites.profile_for(&node.id);
let Some(site) = sites.site_profile(&site_id) else {
refusals.push(PlacementRefusal {
node: node.id.clone(),
site: site_id,
reason: PlacementRefusalReason::UnknownSite,
});
continue;
};
if requires_audio_clock(&profile) && !site.is_audio_clock() {
refusals.push(PlacementRefusal {
node: node.id.clone(),
site: site_id.clone(),
reason: PlacementRefusalReason::RealtimePinViolation,
});
}
if !site.supports_latency_class(profile.rate_contract().latency_class()) {
refusals.push(PlacementRefusal {
node: node.id.clone(),
site: site_id,
reason: PlacementRefusalReason::UnsupportedLatencyClass,
});
}
}
refusals
}
fn domain_bridges(graph: &CompiledGraph, sites: &SiteMap) -> Vec<DomainBridge> {
graph
.edges
.iter()
.filter_map(|edge| {
let from_node = &graph.nodes[edge.from_node].id;
let to_node = &graph.nodes[edge.to_node].id;
let from_site = sites.site_for(from_node);
let to_site = sites.site_for(to_node);
let from_profile = sites.profile_for(from_node);
let to_profile = sites.profile_for(to_node);
bridge_descriptor(
from_profile.rate_contract(),
to_profile.rate_contract(),
from_site != to_site,
)
.map(|descriptor| DomainBridge {
edge: edge.id,
from: from_node.clone(),
to: to_node.clone(),
from_site: from_site.clone(),
to_site: to_site.clone(),
descriptor,
})
})
.collect()
}
fn bridge_descriptor(
from: RateContract,
to: RateContract,
crosses_site: bool,
) -> Option<DomainBridgeDescriptor> {
if !crosses_site && from.is_compatible_with(to) {
return None;
}
match (from.clock_domain(), to.clock_domain()) {
(ClockDomain::Sample, ClockDomain::Sample)
if from.nominal_rate_hz() != to.nominal_rate_hz() =>
{
Some(
DomainBridgeDescriptor::resampler(
from.nominal_rate_hz().unwrap_or(1),
to.nominal_rate_hz().unwrap_or(1),
)
.expect("planner supplies nonzero fallback resampler rates"),
)
}
(ClockDomain::Control | ClockDomain::MidiTick, ClockDomain::Block) => Some(
DomainBridgeDescriptor::event_rate_gate(from.clock_domain())
.expect("planner only requests event-rate gates for supported event domains"),
),
(ClockDomain::Wall, _) | (_, ClockDomain::Wall) => {
Some(DomainBridgeDescriptor::jitter_buffer(1))
}
_ if crosses_site || !from.is_compatible_with(to) => {
Some(DomainBridgeDescriptor::latency_comp_delay(0))
}
_ => None,
}
}
fn latency_budget(
graph: &CompiledGraph,
sites: &SiteMap,
bridges: &[DomainBridge],
) -> Vec<PortLatency> {
let bridge_latency = bridges
.iter()
.map(|bridge| (bridge.edge, bridge.descriptor.latency()))
.collect::<BTreeMap<_, _>>();
let mut budgets = vec![BridgeLatency::zero(); graph.nodes.len()];
for node_index in 0..graph.nodes.len() {
let node = &graph.nodes[node_index];
budgets[node_index] = budgets[node_index].plus(sites.profile_for(&node.id).latency());
for edge_index in &graph.outgoing_edges[node_index] {
let edge = &graph.edges[*edge_index];
let candidate = budgets[node_index].plus(
*bridge_latency
.get(&edge.id)
.unwrap_or(&BridgeLatency::zero()),
);
let target = &mut budgets[edge.to_node];
*target = max_latency(*target, candidate);
}
}
graph
.output_nodes
.iter()
.map(|node_index| {
let node = &graph.nodes[*node_index];
let profile = sites.profile_for(&node.id);
PortLatency {
node: node.id.clone(),
site: sites.site_for(&node.id).clone(),
latency: budgets[*node_index],
latency_class: profile.rate_contract().latency_class(),
}
})
.collect()
}
fn requires_audio_clock(profile: &PlacementNodeProfile) -> bool {
profile.realtime_pin() || profile.rate_contract().clock_domain() == ClockDomain::Sample
}
fn max_latency(left: BridgeLatency, right: BridgeLatency) -> BridgeLatency {
BridgeLatency::frames_and_packets(
left.frame_count().max(right.frame_count()),
left.packet_count().max(right.packet_count()),
)
}
impl DomainBridge {
pub fn kind(&self) -> DomainBridgeKind {
self.descriptor.kind()
}
}