Skip to main content

dml_config/
parser.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::{AggregateEntry, BoardConfig, Device, Iommu, IommuType, ResourceEntry};
6use anyhow::{Context, anyhow, bail};
7use fdf_fidl::DriverChannel;
8use fidl_fuchsia_driver_metadata_common as fdr;
9use fidl_next_fuchsia_driver_framework as fdf_framework;
10use fidl_next_fuchsia_hardware_platform_bus as fpbus;
11use fidl_next_fuchsia_hardware_power as fpower;
12use futures::future::try_join_all;
13use phf;
14use std::collections::{HashMap, HashSet};
15use zx;
16
17pub const BIND_PROTOCOL_DEVICE: u32 = 85; // 0x55
18pub const BIND_PLATFORM_DEV_VID_GENERIC: u32 = 0;
19pub const BIND_PLATFORM_DEV_DID_DEVICETREE: u32 = 50; // 0x32
20
21pub fn property_int(val: u32) -> fdf_framework::NodePropertyValue {
22    fdf_framework::NodePropertyValue::IntValue(val)
23}
24
25pub fn property_string(val: &str) -> fdf_framework::NodePropertyValue {
26    fdf_framework::NodePropertyValue::StringValue(val.to_string())
27}
28
29pub fn property_bool(val: bool) -> fdf_framework::NodePropertyValue {
30    fdf_framework::NodePropertyValue::BoolValue(val)
31}
32
33fn map_interrupt_mode(mode: Option<&str>) -> fpbus::ZirconInterruptMode {
34    match mode {
35        Some("EdgeLow") => fpbus::ZirconInterruptMode::EdgeLow,
36        Some("EdgeHigh") => fpbus::ZirconInterruptMode::EdgeHigh,
37        Some("LevelLow") => fpbus::ZirconInterruptMode::LevelLow,
38        Some("LevelHigh") => fpbus::ZirconInterruptMode::LevelHigh,
39        Some("EdgeBoth") => fpbus::ZirconInterruptMode::EdgeBoth,
40        _ => fpbus::ZirconInterruptMode::Default,
41    }
42}
43
44pub type DriverSpecificMetadata =
45    phf::Map<&'static str, &'static [(&'static str, fn() -> anyhow::Result<Vec<u8>>)]>;
46
47pub type DriverSpecificPowerConfigs =
48    phf::Map<&'static str, fn() -> anyhow::Result<Vec<fpower::PowerElementConfiguration>>>;
49
50pub fn make_accept_bind_rule(
51    key: &str,
52    value: fdf_framework::NodePropertyValue,
53) -> fdf_framework::BindRule2 {
54    fdf_framework::BindRule2 {
55        key: key.to_string(),
56        condition: fdf_framework::Condition::Accept,
57        values: vec![value],
58    }
59}
60
61pub fn make_property2(
62    key: &str,
63    value: fdf_framework::NodePropertyValue,
64) -> fdf_framework::NodeProperty2 {
65    fdf_framework::NodeProperty2 { key: key.to_string(), value }
66}
67
68pub fn add_rule_and_property(
69    bind_rules: &mut Vec<fdf_framework::BindRule2>,
70    properties: &mut Vec<fdf_framework::NodeProperty2>,
71    key: &str,
72    value: fdf_framework::NodePropertyValue,
73) {
74    bind_rules.push(make_accept_bind_rule(key, value.clone()));
75    properties.push(make_property2(key, value));
76}
77
78#[derive(Clone, Copy, Debug)]
79pub enum ValueSource {
80    /// Value of a given key within the constraint table.
81    ConstraintKey(&'static str),
82
83    /// The `name` field of the DML service offer.
84    ResourceName,
85
86    /// The target node name (the `to` field) of the DML service offer.
87    ResourceNode,
88
89    /// A literal integer value.
90    Integer(u32),
91
92    /// The `id` field of the provider (the `from` field) device in the DML.
93    ProviderId,
94}
95
96#[derive(Clone, Copy, Debug)]
97pub enum RuleValueType {
98    Integer,
99    String,
100    Bool,
101}
102
103#[derive(Clone, Copy, Debug)]
104pub enum Destination {
105    /// Create a bind rule of the given key and value.
106    BindRules,
107
108    /// Create a node property for the given key and value.
109    Properties,
110
111    /// Create a node property and a bind rule for the given key and value.
112    Both,
113}
114
115/// Defines how to create a node property or bind rule from a given DML service offer.
116#[derive(Clone, Copy, Debug)]
117pub struct PropertyRule {
118    /// Name of key for the node property or bind rule.
119    pub bind_key: &'static str,
120
121    /// Candidates for the value of the node property or bind rule.
122    ///
123    /// Ordered by priority. If the first source exists it will be used, otherwise the next source
124    /// is checked and so on.
125    pub sources: &'static [ValueSource],
126
127    /// The type of the node property's or bind rule's value.
128    pub value_type: RuleValueType,
129
130    /// Where to add `bind_key` and its value in the composite node spec.
131    pub destination: Destination,
132}
133
134#[derive(Clone, Copy, Debug)]
135pub enum TransportType {
136    None,
137    Zircon,
138    Driver,
139}
140
141/// Defines how to convert a given DML service offer into a parent node of a composite node spec.
142#[derive(Clone, Copy, Debug)]
143pub struct ServiceBindConfig {
144    /// Which transport type to use.
145    pub transport: TransportType,
146
147    /// Rules for creating node properties and bind rules for the parent node.
148    pub rules: &'static [PropertyRule],
149
150    /// Candidates for the deduplication key of the parent node.
151    ///
152    /// Ordered by priority. If the first source exists it will be used, otherwise the next source
153    /// is checked and so on.
154    pub parent_key_sources: &'static [ValueSource],
155
156    /// If true, adds a `fuchsia.ID` bind rule from the `"id"` constraint when not already set by
157    /// `rules`.
158    pub bind_id: bool,
159}
160
161pub const DEFAULT_SERVICE_BIND_CONFIG: ServiceBindConfig = ServiceBindConfig {
162    transport: TransportType::Zircon,
163    rules: &[],
164    parent_key_sources: &[ValueSource::ResourceName],
165    bind_id: false,
166};
167
168pub const DEFAULT_ID_SERVICE_BIND_CONFIG: ServiceBindConfig =
169    ServiceBindConfig { bind_id: true, ..DEFAULT_SERVICE_BIND_CONFIG };
170
171pub struct DmlParserConfig {
172    pub service_configs: phf::Map<&'static str, ServiceBindConfig>,
173}
174
175/// A list of instructions for how to convert common DML service offers into parent nodes
176/// of composite node specs.
177pub static STANDARD_SERVICE_CONFIGS: phf::Map<&'static str, ServiceBindConfig> = phf::phf_map! {
178    "fuchsia.clock.Init" => ServiceBindConfig {
179        transport: TransportType::None,
180        rules: &[PropertyRule {
181            bind_key: "fuchsia.BIND_INIT_STEP",
182            sources: &[ValueSource::Integer(0x494B4C43)],
183            value_type: RuleValueType::Integer,
184            destination: Destination::Both,
185        }],
186        ..DEFAULT_SERVICE_BIND_CONFIG
187    },
188    "fuchsia.pwm.Init" => ServiceBindConfig {
189        transport: TransportType::None,
190        rules: &[PropertyRule {
191            bind_key: "fuchsia.BIND_INIT_STEP",
192            sources: &[ValueSource::Integer(0x004D5750)],
193            value_type: RuleValueType::Integer,
194            destination: Destination::Both,
195        }],
196        ..DEFAULT_SERVICE_BIND_CONFIG
197    },
198    "fuchsia.gpio.Init" => ServiceBindConfig {
199        transport: TransportType::None,
200        rules: &[PropertyRule {
201            bind_key: "fuchsia.BIND_INIT_STEP",
202            sources: &[ValueSource::Integer(0x4F495047)],
203            value_type: RuleValueType::Integer,
204            destination: Destination::Both,
205        }],
206        ..DEFAULT_SERVICE_BIND_CONFIG
207    },
208    "fuchsia.hardware.gpu.mali.Service" => ServiceBindConfig {
209        transport: TransportType::Driver,
210        ..DEFAULT_SERVICE_BIND_CONFIG
211    },
212    "fuchsia.hardware.platform.device.Service" => ServiceBindConfig {
213        rules: &[
214            PropertyRule {
215                bind_key: "fuchsia.BIND_PROTOCOL",
216                sources: &[ValueSource::Integer(BIND_PROTOCOL_DEVICE)],
217                value_type: RuleValueType::Integer,
218                destination: Destination::Both,
219            },
220            PropertyRule {
221                bind_key: "fuchsia.BIND_PLATFORM_DEV_VID",
222                sources: &[ValueSource::ConstraintKey("vid")],
223                value_type: RuleValueType::Integer,
224                destination: Destination::Both,
225            },
226            PropertyRule {
227                bind_key: "fuchsia.BIND_PLATFORM_DEV_DID",
228                sources: &[ValueSource::ConstraintKey("did")],
229                value_type: RuleValueType::Integer,
230                destination: Destination::Both,
231            },
232        ],
233        ..DEFAULT_SERVICE_BIND_CONFIG
234    },
235    "fuchsia.hardware.platform.bus.Service" => ServiceBindConfig {
236        transport: TransportType::Driver,
237        rules: &[
238            PropertyRule {
239                bind_key: "fuchsia.BIND_PLATFORM_DEV_VID",
240                sources: &[ValueSource::ConstraintKey("vid")],
241                value_type: RuleValueType::Integer,
242                destination: Destination::Both,
243            },
244            PropertyRule {
245                bind_key: "fuchsia.BIND_PLATFORM_DEV_PID",
246                sources: &[ValueSource::ConstraintKey("pid")],
247                value_type: RuleValueType::Integer,
248                destination: Destination::Both,
249            },
250            PropertyRule {
251                bind_key: "fuchsia.BIND_PLATFORM_DEV_DID",
252                sources: &[ValueSource::ConstraintKey("did")],
253                value_type: RuleValueType::Integer,
254                destination: Destination::Both,
255            },
256        ],
257        ..DEFAULT_SERVICE_BIND_CONFIG
258    },
259    "fuchsia.hardware.pci.Service" => ServiceBindConfig {
260        rules: &[
261            PropertyRule {
262                bind_key: "fuchsia.BIND_PCI_VID",
263                sources: &[ValueSource::ConstraintKey("vid")],
264                value_type: RuleValueType::Integer,
265                destination: Destination::Both,
266            },
267            PropertyRule {
268                bind_key: "fuchsia.BIND_PCI_DID",
269                sources: &[ValueSource::ConstraintKey("did")],
270                value_type: RuleValueType::Integer,
271                destination: Destination::Both,
272            },
273            PropertyRule {
274                bind_key: "fuchsia.BIND_PCI_TOPO",
275                sources: &[ValueSource::ConstraintKey("topo")],
276                value_type: RuleValueType::Integer,
277                destination: Destination::BindRules,
278            },
279        ],
280        ..DEFAULT_SERVICE_BIND_CONFIG
281    },
282};
283
284pub const DEFAULT_DML_PARSER_CONFIG: DmlParserConfig =
285    DmlParserConfig { service_configs: phf::phf_map! {} };
286
287impl Default for DmlParserConfig {
288    fn default() -> Self {
289        DEFAULT_DML_PARSER_CONFIG
290    }
291}
292
293fn resolve_value(
294    provider_id: Option<u32>,
295    source: &ValueSource,
296    res: &ResourceEntry,
297    constraint: &fdr::Dictionary,
298) -> Option<ResolvedValue> {
299    match source {
300        ValueSource::ConstraintKey(key) => {
301            if let Some(i) = crate::get_int64(constraint, key) {
302                Some(ResolvedValue::Integer(i as u32))
303            } else if let Some(s) = crate::get_string(constraint, key) {
304                Some(ResolvedValue::String(s))
305            } else if let Some(b) = crate::get_bool(constraint, key) {
306                Some(ResolvedValue::Bool(b))
307            } else {
308                None
309            }
310        }
311        ValueSource::ResourceName => res.name.clone().map(ResolvedValue::String),
312        ValueSource::ResourceNode => res.node.clone().map(ResolvedValue::String),
313        ValueSource::Integer(i) => Some(ResolvedValue::Integer(*i)),
314        ValueSource::ProviderId => provider_id.map(ResolvedValue::Integer),
315    }
316}
317
318enum ResolvedValue {
319    Integer(u32),
320    String(String),
321    Bool(bool),
322}
323
324fn apply_rule(
325    provider_id: Option<u32>,
326    rule: &PropertyRule,
327    res: &ResourceEntry,
328    constraint: &fdr::Dictionary,
329    bind_rules: &mut Vec<fdf_framework::BindRule2>,
330    properties: &mut Vec<fdf_framework::NodeProperty2>,
331) -> anyhow::Result<()> {
332    let resolved =
333        rule.sources.iter().find_map(|source| resolve_value(provider_id, source, res, constraint));
334
335    let Some(val) = resolved else {
336        return Ok(());
337    };
338
339    let property_value = match (val, rule.value_type) {
340        (ResolvedValue::Integer(i), RuleValueType::Integer) => property_int(i),
341        (ResolvedValue::String(s), RuleValueType::String) => property_string(&s),
342        (ResolvedValue::Bool(b), RuleValueType::Bool) => property_bool(b),
343        (ResolvedValue::String(s), RuleValueType::Integer) => {
344            let parsed = if s.starts_with("0x") || s.starts_with("0X") {
345                u32::from_str_radix(&s[2..], 16)
346            } else {
347                s.parse::<u32>()
348            };
349            match parsed {
350                Ok(i) => property_int(i),
351                Err(e) => {
352                    bail!("Failed to parse string {} as integer: {}", s, e);
353                }
354            }
355        }
356        (ResolvedValue::String(s), RuleValueType::Bool) => match s.parse::<bool>() {
357            Ok(b) => property_bool(b),
358            Err(e) => {
359                bail!("Failed to parse string {} as bool: {}", s, e);
360            }
361        },
362        _ => {
363            bail!("Type mismatch in rule resolution");
364        }
365    };
366
367    match rule.destination {
368        Destination::BindRules => {
369            bind_rules.push(make_accept_bind_rule(rule.bind_key, property_value));
370        }
371        Destination::Properties => {
372            properties.push(make_property2(rule.bind_key, property_value));
373        }
374        Destination::Both => {
375            add_rule_and_property(bind_rules, properties, rule.bind_key, property_value);
376        }
377    }
378
379    Ok(())
380}
381
382pub fn generate_parent_spec_generic(
383    agg: &AggregateEntry,
384    res: &ResourceEntry,
385    provider_id: Option<u32>,
386    parser_config: &DmlParserConfig,
387) -> anyhow::Result<Option<(fdf_framework::ParentSpec2, String)>> {
388    let mut bind_rules = Vec::new();
389    let mut properties = Vec::new();
390
391    let service_name = agg.service.as_deref().unwrap_or("");
392    let service_config = parser_config
393        .service_configs
394        .get(service_name)
395        .or_else(|| STANDARD_SERVICE_CONFIGS.get(service_name))
396        .unwrap_or(&DEFAULT_ID_SERVICE_BIND_CONFIG);
397
398    match service_config.transport {
399        TransportType::Zircon => {
400            add_rule_and_property(
401                &mut bind_rules,
402                &mut properties,
403                "fuchsia.Service",
404                property_string(service_name),
405            );
406            properties.push(make_property2(
407                service_name,
408                property_string(&format!("{service_name}.ZirconTransport")),
409            ));
410        }
411        TransportType::Driver => {
412            add_rule_and_property(
413                &mut bind_rules,
414                &mut properties,
415                "fuchsia.Service",
416                property_string(service_name),
417            );
418            properties.push(make_property2(
419                service_name,
420                property_string(&format!("{service_name}.DriverTransport")),
421            ));
422        }
423        TransportType::None => {}
424    }
425
426    let constraint =
427        res.constraint.as_ref().ok_or_else(|| anyhow!("constraint is missing in ResourceEntry"))?;
428
429    for rule in service_config.rules {
430        apply_rule(provider_id, rule, res, constraint, &mut bind_rules, &mut properties)?;
431    }
432
433    if !properties.iter().any(|p| p.key == "fuchsia.NAME") {
434        let name_opt = res.name.clone().or_else(|| crate::get_string(constraint, "name"));
435        if let Some(name) = name_opt {
436            properties.push(make_property2("fuchsia.NAME", property_string(&name)));
437        }
438    }
439
440    if service_config.bind_id
441        && !bind_rules.iter().any(|r| r.key == "fuchsia.ID")
442        && let Some(id) = crate::get_uint32(constraint, "id")
443    {
444        bind_rules.push(make_accept_bind_rule("fuchsia.ID", property_int(id)));
445    }
446
447    let resolved_key = service_config.parent_key_sources.iter().find_map(|source| {
448        match resolve_value(provider_id, source, res, constraint) {
449            Some(ResolvedValue::String(k)) => Some(k),
450            _ => None,
451        }
452    });
453
454    let key = resolved_key.with_context(|| {
455        format!(
456            "Failed to resolve parent key for service {}. If this service should be ignored, add it to the config.",
457            service_name
458        )
459    })?;
460
461    let parent = fdf_framework::ParentSpec2 { bind_rules, properties };
462    Ok(Some((parent, key)))
463}
464
465/// Returns the ID of the provider of `agg` by looking it up in `devices`.
466///
467/// Returns [`None`] if the provider is not found in `devices` or does not have an ID.
468pub fn get_provider_id(agg: &AggregateEntry, devices: &[Device]) -> Option<u32> {
469    let device = devices.iter().find(|d| d.name.as_deref() == agg.provider.as_deref());
470    let id = device.and_then(|d| d.id);
471    log::debug!(
472        "get_provider_id: provider={:?}, device_found={}, device_id={:?}",
473        agg.provider,
474        device.is_some(),
475        id,
476    );
477    id
478}
479
480pub async fn publish_dml_devices(
481    pbus: &fidl_next::Client<fpbus::PlatformBus, DriverChannel>,
482    composite_manager: &fidl_next::Client<fdf_framework::CompositeNodeManager, zx::Channel>,
483    config: &BoardConfig,
484    parser_config: &DmlParserConfig,
485    driver_metadata: Option<&DriverSpecificMetadata>,
486    driver_power_configs: Option<&DriverSpecificPowerConfigs>,
487    enabled_nodes: &[String],
488) -> anyhow::Result<()> {
489    let mut provider_metadata = HashMap::<String, Vec<fpbus::Metadata>>::new();
490
491    register_iommus(pbus, config.iommus.as_deref()).await.context("Failed to register IOMMUs")?;
492
493    let devices = config
494        .devices
495        .as_ref()
496        .ok_or_else(|| anyhow!("devices field is missing in BoardConfig"))?;
497
498    let disabled_devices: HashSet<&str> = devices
499        .iter()
500        .filter(|d| crate::is_device_disabled(d, enabled_nodes))
501        .filter_map(|d| d.name.as_deref())
502        .collect();
503
504    // 1. Generate driver specific metadata for devices in config
505    if let Some(drv_meta) = driver_metadata {
506        for dev in devices {
507            let name = dev.name.as_deref().unwrap_or("");
508            if disabled_devices.contains(name) {
509                continue;
510            }
511            if let Some(generators) = drv_meta.get(name) {
512                for (metadata_id, gen_fn) in *generators {
513                    let data = gen_fn()
514                        .with_context(|| format!("Failed to generate metadata for {}", name))?;
515                    provider_metadata.entry(name.to_string()).or_default().push(fpbus::Metadata {
516                        id: Some(metadata_id.to_string()),
517                        data: Some(data),
518                        ..Default::default()
519                    });
520                }
521            }
522        }
523    }
524
525    for (idx, dev) in devices.iter().enumerate() {
526        let dev_name = dev.name.as_deref().unwrap_or("");
527        if disabled_devices.contains(dev_name) {
528            continue;
529        }
530        if dev.disabled.unwrap_or(false) {
531            log::info!("Publishing disabled DML device '{}' due to runtime override", dev_name);
532        }
533        let instance_id = idx as u32 + 1;
534        let mut node = fpbus::Node {
535            name: dev.name.clone(),
536            vid: Some(0),
537            pid: Some(0),
538            did: Some(0),
539            instance_id: Some(instance_id),
540            driver_host: dev.driver_host.clone(),
541            ..Default::default()
542        };
543        node.interrupt_controller_id = dev.interrupt_controller_id;
544
545        if let Some(compatible) = &dev.compatible {
546            node.properties = Some(vec![fdf_framework::NodeProperty2 {
547                key: "fuchsia.COMPATIBLE".to_string(),
548                value: fdf_framework::NodePropertyValue::StringValue(compatible.clone()),
549            }]);
550            node.did = Some(BIND_PLATFORM_DEV_DID_DEVICETREE);
551            node.vid = Some(BIND_PLATFORM_DEV_VID_GENERIC);
552        }
553
554        let mut mmio_list = Vec::new();
555        let mut irq_list = Vec::new();
556        let mut irq_controllers = Vec::new();
557        let mut bti_list = Vec::new();
558        let mut smc_list = Vec::new();
559        let mut boot_metadata_list = Vec::new();
560
561        if let Some(pdev_dict) = crate::pdev_constraints(config, dev.name.as_deref().unwrap_or(""))
562        {
563            for mmio in crate::mmio_list(pdev_dict) {
564                mmio_list.push(fpbus::Mmio {
565                    base: Some(mmio.base),
566                    length: Some(mmio.length),
567                    name: mmio.name,
568                    ..Default::default()
569                });
570            }
571            for irq in crate::irq_list(pdev_dict) {
572                let irq_spec = match irq.controller {
573                    Some(controller_id) => {
574                        if !irq_controllers.contains(&controller_id) {
575                            irq_controllers.push(controller_id);
576                        }
577                        fpbus::IrqSpec::UserspaceIrq(fpbus::UserspaceIrq {
578                            irq: irq.number,
579                            controller_id,
580                        })
581                    }
582                    None => fpbus::IrqSpec::Irq(irq.number),
583                };
584                irq_list.push(fpbus::Irq {
585                    irq: Some(irq_spec),
586                    mode: Some(map_interrupt_mode(irq.mode.as_deref())),
587                    name: irq.name.clone(),
588                    wake_vector: irq.wake_vector,
589                    ..Default::default()
590                });
591            }
592            for bti in crate::bti_list(pdev_dict)? {
593                bti_list.push(fpbus::Bti {
594                    iommu_id: Some(bti.iommu_id),
595                    bti_id: Some(bti.id),
596                    name: bti.name,
597                    ..Default::default()
598                });
599            }
600            for smc in crate::smc_list(pdev_dict) {
601                smc_list.push(fpbus::Smc {
602                    service_call_num_base: Some(smc.service_call_num_base),
603                    count: Some(smc.count),
604                    exclusive: Some(smc.exclusive),
605                    name: smc.name,
606                    ..Default::default()
607                });
608            }
609            for bm in crate::boot_metadata_list(pdev_dict) {
610                boot_metadata_list.push(fpbus::BootMetadata {
611                    zbi_type: Some(bm.zbi_type),
612                    zbi_extra: bm.zbi_extra.or(Some(0)),
613                    ..Default::default()
614                });
615            }
616        }
617
618        if !mmio_list.is_empty() {
619            node.mmio = Some(mmio_list);
620        }
621        if !irq_list.is_empty() {
622            node.irq = Some(irq_list);
623        }
624        if !bti_list.is_empty() {
625            node.bti = Some(bti_list);
626        }
627        if !smc_list.is_empty() {
628            node.smc = Some(smc_list);
629        }
630        if !boot_metadata_list.is_empty() {
631            node.boot_metadata = Some(boot_metadata_list);
632        }
633
634        let mut metadata_list = Vec::new();
635
636        // Handle static metadata from config
637        if let Some(metadata) = &dev.metadata {
638            for meta in metadata {
639                let data =
640                    meta.data.clone().ok_or_else(|| anyhow!("Static metadata missing data"))?;
641                metadata_list.push(fpbus::Metadata {
642                    id: meta.id.clone(),
643                    data: Some(data),
644                    ..Default::default()
645                });
646            }
647        }
648
649        if let Some(meta) = provider_metadata.get(dev_name) {
650            metadata_list.extend(meta.clone());
651        }
652
653        if !metadata_list.is_empty() {
654            node.metadata = Some(metadata_list);
655        }
656
657        if let Some(gen_fn) = driver_power_configs.and_then(|configs| configs.get(dev_name)) {
658            let power_config = gen_fn()
659                .with_context(|| format!("Failed to generate power config for {}", dev_name))?;
660            if !power_config.is_empty() {
661                node.power_config = Some(power_config);
662            }
663        }
664
665        let mut resource_parents = Vec::new();
666        let mut generated_keys = HashSet::new();
667        for agg in config.aggregates.iter().flatten() {
668            if agg.provider.as_deref().is_some_and(|p| disabled_devices.contains(p)) {
669                continue;
670            }
671            if dev.compatible.is_some()
672                && matches!(
673                    agg.service.as_deref(),
674                    Some("fuchsia.hardware.platform.device.Service")
675                        | Some("fuchsia.hardware.interrupt.ControllerRegistryService")
676                )
677            {
678                continue;
679            }
680            let provider_id = get_provider_id(agg, devices);
681            for res in agg
682                .resources
683                .iter()
684                .flatten()
685                .filter(|res| res.node.as_deref() == dev.name.as_deref())
686            {
687                if let Some((parent, key)) =
688                    generate_parent_spec_generic(agg, res, provider_id, parser_config)?
689                {
690                    if generated_keys.insert(key) {
691                        if matches!(agg.provider.as_deref(), Some("parent") | Some("pdev")) {
692                            resource_parents.insert(0, parent);
693                        } else {
694                            resource_parents.push(parent);
695                        }
696                    }
697                }
698            }
699        }
700
701        for (parent, key) in generate_interrupt_controller_parents(&irq_controllers) {
702            if generated_keys.insert(key) {
703                resource_parents.push(parent);
704            }
705        }
706
707        if resource_parents.is_empty() && dev.driver_host.is_some() {
708            node.driver_host = dev.driver_host.clone();
709        }
710
711        let mut spec = fdf_framework::CompositeNodeSpec {
712            name: dev.name.clone(),
713            driver_host: dev.driver_host.clone(),
714            ..Default::default()
715        };
716
717        let mut parents2 = Vec::new();
718
719        // 1. Generate pdev parent if compatible is present
720        if let Some(compatible) = &dev.compatible {
721            let pdev_parent = fdf_framework::ParentSpec2 {
722                bind_rules: vec![
723                    make_accept_bind_rule(
724                        "fuchsia.BIND_PROTOCOL",
725                        property_int(BIND_PROTOCOL_DEVICE),
726                    ),
727                    make_accept_bind_rule(
728                        "fuchsia.BIND_PLATFORM_DEV_VID",
729                        property_int(BIND_PLATFORM_DEV_VID_GENERIC),
730                    ),
731                    make_accept_bind_rule(
732                        "fuchsia.BIND_PLATFORM_DEV_DID",
733                        property_int(BIND_PLATFORM_DEV_DID_DEVICETREE),
734                    ),
735                    make_accept_bind_rule(
736                        "fuchsia.BIND_PLATFORM_DEV_INSTANCE_ID",
737                        property_int(instance_id),
738                    ),
739                    make_accept_bind_rule("fuchsia.COMPATIBLE", property_string(compatible)),
740                ],
741                properties: vec![
742                    // TODO(https://fxbug.dev/555962083): Restore `fuchsia.NAME = "pdev"` once all
743                    // composite drivers are migrated from `primary parent "devicetree"` to `"pdev"`
744                    // (`driver-index` rejects `ParentSpec2.properties` when `fuchsia.NAME` does not
745                    // equal the `.bind` primary parent symbol name).
746                    make_property2("fuchsia.BIND_PROTOCOL", property_int(BIND_PROTOCOL_DEVICE)),
747                    make_property2(
748                        "fuchsia.BIND_PLATFORM_DEV_VID",
749                        property_int(BIND_PLATFORM_DEV_VID_GENERIC),
750                    ),
751                    make_property2(
752                        "fuchsia.BIND_PLATFORM_DEV_DID",
753                        property_int(BIND_PLATFORM_DEV_DID_DEVICETREE),
754                    ),
755                    make_property2(
756                        "fuchsia.BIND_PLATFORM_DEV_INSTANCE_ID",
757                        property_int(instance_id),
758                    ),
759                    make_property2("fuchsia.COMPATIBLE", property_string(compatible)),
760                    make_property2(
761                        "fuchsia.devicetree.FIRST_COMPATIBLE",
762                        property_string(compatible),
763                    ),
764                    make_property2(
765                        "fuchsia.Service",
766                        property_string("fuchsia.hardware.platform.device.Service"),
767                    ),
768                ],
769            };
770            parents2.push(pdev_parent);
771        }
772
773        parents2.extend(resource_parents);
774
775        if !parents2.is_empty() {
776            spec.parents2 = Some(parents2);
777
778            log::debug!(
779                "DML-CONFIG: Adding composite spec: name={:?}, parents2={:?}",
780                spec.name,
781                spec.parents2
782            );
783            composite_manager
784                .add_spec_with(spec)
785                .await
786                .context("AddSpec request failed")?
787                .map_err(|e| anyhow!("AddSpec failed: {e:?}"))?;
788        }
789
790        if dev.compatible.is_some() {
791            log::debug!("DML-CONFIG: Adding node: {}", dev_name);
792            pbus.node_add(node)
793                .await
794                .context("NodeAdd request failed")?
795                .context("NodeAdd failed")?;
796        }
797    }
798
799    Ok(())
800}
801
802/// Generates composite parent specifications for distinct userspace interrupt controllers
803/// referenced by a device's interrupts, in first-reference order.
804fn generate_interrupt_controller_parents(
805    irq_controllers: &[u32],
806) -> Vec<(fdf_framework::ParentSpec2, String)> {
807    irq_controllers
808        .iter()
809        .enumerate()
810        .map(|(idx, &controller_id)| {
811            let key = format!("interrupt-controller-{idx}");
812            let parent = fdf_framework::ParentSpec2 {
813                bind_rules: vec![
814                    make_accept_bind_rule("fuchsia.NAME", property_string("interrupt-controller")),
815                    make_accept_bind_rule("fuchsia.ID", property_int(controller_id)),
816                ],
817                properties: vec![make_property2("fuchsia.NAME", property_string(&key))],
818            };
819            (parent, key)
820        })
821        .collect()
822}
823
824/// Registers `iommus` with the `pbus`.
825async fn register_iommus(
826    pbus: &fidl_next::Client<fpbus::PlatformBus, DriverChannel>,
827    iommus: Option<&[Iommu]>,
828) -> anyhow::Result<()> {
829    let Some(iommus) = iommus else {
830        return Ok(());
831    };
832
833    let futures = iommus
834        .iter()
835        .map(|iommu| {
836            let iommu_name = iommu.name.as_deref().unwrap_or("unnamed");
837            let iommu_id = iommu.id.with_context(|| format!("IOMMU {iommu_name:?} missing id"))?;
838            let fpbus_iommu = match &iommu.iommu_type {
839                Some(IommuType::ArmSmmu(arm_smmu)) => {
840                    fpbus::Iommu::ArmSmmu(fpbus::ArmSmmu { base_address: arm_smmu.base_address })
841                }
842                Some(IommuType::StubIommu(_)) | None => fpbus::Iommu::StubIommu(()),
843                _ => {
844                    bail!("Unsupported IOMMU type for IOMMU {iommu_name:?}");
845                }
846            };
847            let future = async move {
848                log::info!("Registering IOMMU '{iommu_name}' (id: {iommu_id})");
849                pbus.register_iommu(iommu_id, &fpbus_iommu)
850                    .await
851                    .with_context(|| {
852                        format!(
853                            "Failed to send RegisterIommu FIDL request for IOMMU {iommu_name:?}"
854                        )
855                    })?
856                    .with_context(|| format!("Failed to register IOMMU {iommu_name:?}"))?;
857                Ok::<(), anyhow::Error>(())
858            };
859            Ok(future)
860        })
861        .collect::<Result<Vec<_>, _>>()?;
862    try_join_all(futures).await?;
863    Ok(())
864}
865
866#[cfg(test)]
867mod tests {
868    use super::*;
869
870    #[test]
871    fn test_pci_service_parent_spec() {
872        let res = ResourceEntry {
873            name: Some("pci".to_string()),
874            node: Some("mali-gpu".to_string()),
875            constraint: Some(fdr::Dictionary {
876                entries: Some(vec![
877                    fdr::DictionaryEntry {
878                        key: "vid".to_string(),
879                        value: fdr::DictionaryValue::Int64(0x13b5),
880                    },
881                    fdr::DictionaryEntry {
882                        key: "did".to_string(),
883                        value: fdr::DictionaryValue::Int64(0x7212),
884                    },
885                    fdr::DictionaryEntry {
886                        key: "topo".to_string(),
887                        value: fdr::DictionaryValue::Int64(256),
888                    },
889                ]),
890                ..Default::default()
891            }),
892            ..Default::default()
893        };
894
895        let agg = AggregateEntry {
896            provider: Some("pcie-c500000".to_string()),
897            service: Some("fuchsia.hardware.pci.Service".to_string()),
898            resources: Some(vec![res.clone()]),
899            ..Default::default()
900        };
901
902        let (parent, key) =
903            generate_parent_spec_generic(&agg, &res, None, &DEFAULT_DML_PARSER_CONFIG)
904                .expect("should succeed")
905                .expect("should return parent spec");
906
907        assert_eq!(key, "pci");
908        assert!(parent.bind_rules.iter().any(|r| r.key == "fuchsia.BIND_PCI_VID"
909            && r.values == vec![fdf_framework::NodePropertyValue::IntValue(0x13b5)]));
910        assert!(parent.bind_rules.iter().any(|r| r.key == "fuchsia.BIND_PCI_DID"
911            && r.values == vec![fdf_framework::NodePropertyValue::IntValue(0x7212)]));
912        assert!(parent.bind_rules.iter().any(|r| r.key == "fuchsia.BIND_PCI_TOPO"
913            && r.values == vec![fdf_framework::NodePropertyValue::IntValue(256)]));
914        assert!(parent.properties.iter().any(|p| p.key == "fuchsia.BIND_PCI_VID"
915            && p.value == fdf_framework::NodePropertyValue::IntValue(0x13b5)));
916        assert!(parent.properties.iter().any(|p| p.key == "fuchsia.BIND_PCI_DID"
917            && p.value == fdf_framework::NodePropertyValue::IntValue(0x7212)));
918        assert!(!parent.properties.iter().any(|p| p.key == "fuchsia.BIND_PCI_TOPO"));
919    }
920
921    #[test]
922    fn test_interrupt_controller_parents_dedup_and_order() {
923        let pdev_dict = fdr::Dictionary {
924            entries: Some(vec![
925                fdr::DictionaryEntry {
926                    key: "interrupts.0.number".to_string(),
927                    value: fdr::DictionaryValue::Int64(574),
928                },
929                fdr::DictionaryEntry {
930                    key: "interrupts.1.number".to_string(),
931                    value: fdr::DictionaryValue::Int64(17),
932                },
933                fdr::DictionaryEntry {
934                    key: "interrupts.1.controller".to_string(),
935                    value: fdr::DictionaryValue::Int64(290),
936                },
937                fdr::DictionaryEntry {
938                    key: "interrupts.2.number".to_string(),
939                    value: fdr::DictionaryValue::Int64(18),
940                },
941                fdr::DictionaryEntry {
942                    key: "interrupts.2.controller".to_string(),
943                    value: fdr::DictionaryValue::Int64(290),
944                },
945                fdr::DictionaryEntry {
946                    key: "interrupts.3.number".to_string(),
947                    value: fdr::DictionaryValue::Int64(4),
948                },
949                fdr::DictionaryEntry {
950                    key: "interrupts.3.controller".to_string(),
951                    value: fdr::DictionaryValue::Int64(289),
952                },
953            ]),
954            ..Default::default()
955        };
956
957        let mut irq_controllers = Vec::new();
958        for irq in crate::irq_list(&pdev_dict) {
959            if let Some(controller_id) = irq.controller {
960                if !irq_controllers.contains(&controller_id) {
961                    irq_controllers.push(controller_id);
962                }
963            }
964        }
965        assert_eq!(irq_controllers, vec![290, 289]);
966
967        let parents = generate_interrupt_controller_parents(&irq_controllers);
968        assert_eq!(parents.len(), 2);
969
970        let (parent0, key0) = &parents[0];
971        assert_eq!(key0, "interrupt-controller-0");
972        assert_eq!(
973            parent0.bind_rules,
974            vec![
975                make_accept_bind_rule("fuchsia.NAME", property_string("interrupt-controller")),
976                make_accept_bind_rule("fuchsia.ID", property_int(290)),
977            ]
978        );
979        assert_eq!(
980            parent0.properties,
981            vec![make_property2("fuchsia.NAME", property_string("interrupt-controller-0"))]
982        );
983
984        let (parent1, key1) = &parents[1];
985        assert_eq!(key1, "interrupt-controller-1");
986        assert_eq!(
987            parent1.bind_rules,
988            vec![
989                make_accept_bind_rule("fuchsia.NAME", property_string("interrupt-controller")),
990                make_accept_bind_rule("fuchsia.ID", property_int(289)),
991            ]
992        );
993        assert_eq!(
994            parent1.properties,
995            vec![make_property2("fuchsia.NAME", property_string("interrupt-controller-1"))]
996        );
997    }
998}