Skip to main content

vm_topology/processor/
aarch64.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! ARM64-specific topology definitions.
5
6use super::ArchTopology;
7use super::InvalidTopology;
8use super::ProcessorTopology;
9use super::THREADS_PER_CORE;
10use super::TopologyBuilder;
11use super::VpIndex;
12use super::VpInfo;
13use super::VpTopologyInfo;
14use aarch64defs::MpidrEl1;
15
16/// ARM64-specific topology information.
17#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
18#[derive(Debug, Copy, Clone)]
19#[non_exhaustive]
20pub struct Aarch64Topology {
21    platform: Aarch64PlatformConfig,
22}
23
24impl ArchTopology for Aarch64Topology {
25    type ArchVpInfo = Aarch64VpInfo;
26    type BuilderState = Aarch64TopologyBuilderState;
27
28    fn vp_topology(topology: &ProcessorTopology<Self>, info: &Self::ArchVpInfo) -> VpTopologyInfo {
29        // MPIDR is an identity, not a topology description: the non-SMT
30        // encoding below packs the VP index without regard to sockets or
31        // cores, and affinity levels only carry their conventional meanings
32        // when MT is set. Ask the configured topology instead of decoding
33        // affinity, accepting that the answer is only as good as that
34        // configuration.
35        topology.logical_topology(info.base.vp_index)
36    }
37}
38
39/// Builds the MPIDR for a VP on a platform without SMT.
40///
41/// `Aff0` holds 16 VPs per affinity group because GICv3 targeted SGIs select
42/// `Aff0` through a 16-bit target list, and reaching beyond that requires
43/// Range Selector Support.
44///
45/// Note that this says nothing about sockets or cores. Logical topology is
46/// reported separately, via [`ProcessorTopology::vp_topology`].
47fn non_smt_mpidr(vp_index: u32) -> MpidrEl1 {
48    MpidrEl1::new()
49        .with_aff0((vp_index % AFF0_PER_GROUP) as u8)
50        .with_aff1((vp_index / AFF0_PER_GROUP) as u8)
51        .with_aff2((vp_index / (AFF0_PER_GROUP << 8)) as u8)
52        .with_aff3((vp_index / (AFF0_PER_GROUP << 16)) as u8)
53}
54
55/// Builds the MPIDR for a VP on a platform with SMT.
56///
57/// `MPIDR.MT` is set, so `Aff0` is a thread id and the core index packs into
58/// the fields above it. Sibling threads therefore share everything above
59/// `Aff0`.
60///
61/// Like the non-SMT encoding, this describes no socket placement; that is
62/// PPTT's job.
63fn smt_mpidr(vp_index: u32) -> MpidrEl1 {
64    let core = vp_index / THREADS_PER_CORE;
65    MpidrEl1::new()
66        .with_mt(true)
67        .with_aff0((vp_index % THREADS_PER_CORE) as u8)
68        .with_aff1(core as u8)
69        .with_aff2((core >> 8) as u8)
70        .with_aff3((core >> 16) as u8)
71}
72
73/// The number of `Aff0` values a GICv3 SGI can target in one affinity group
74/// without Range Selector Support.
75const AFF0_PER_GROUP: u32 = 16;
76
77/// Aarch64-specific [`TopologyBuilder`] state.
78pub struct Aarch64TopologyBuilderState {
79    platform: Aarch64PlatformConfig,
80}
81
82/// GIC version and version-specific addressing for the virtual machine.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
85#[cfg_attr(feature = "inspect", inspect(external_tag))]
86pub enum GicVersion {
87    /// GICv2 — uses a shared CPU interface region instead of per-VP redistributors.
88    /// Required for platforms like Raspberry Pi 5 (GIC-400).
89    V2 {
90        /// Physical base address of the GIC CPU interface.
91        #[cfg_attr(feature = "inspect", inspect(hex))]
92        cpu_interface_base: u64,
93    },
94    /// GICv3 — uses per-VP redistributors. Default for most server/desktop platforms.
95    V3 {
96        /// Physical base address of the GIC redistributor region.
97        #[cfg_attr(feature = "inspect", inspect(hex))]
98        redistributors_base: u64,
99    },
100}
101
102/// ARM64 platform interrupt and GIC configuration.
103///
104/// Groups GIC base addresses, MSI frame info, and platform interrupt
105/// assignments (PMU, virtual timer) into a single struct so that the
106/// topology builder takes one value instead of several positional `u32`s.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
109pub struct Aarch64PlatformConfig {
110    /// GIC distributor base address.
111    #[cfg_attr(feature = "inspect", inspect(hex))]
112    pub gic_distributor_base: u64,
113    /// GIC version and version-specific addresses.
114    pub gic_version: GicVersion,
115    /// MSI controller for PCIe interrupt delivery.
116    pub gic_msi: GicMsiController,
117    /// Performance Monitor Unit GSIV (GIC INTID). `None` if not available.
118    pub pmu_gsiv: Option<u32>,
119    /// Virtual timer PPI (GIC INTID, e.g. 20 for PPI 4).
120    pub virt_timer_ppi: u32,
121    /// Total number of GIC interrupts (SGIs + PPIs + SPIs).
122    ///
123    /// KVM requires: `64 <= gic_nr_irqs <= 1023` and a multiple of 32.
124    /// The maximum valid value is 992 (31 × 32).
125    pub gic_nr_irqs: u32,
126}
127
128/// GIC v2m MSI frame parameters.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
131pub struct GicV2mInfo {
132    /// Physical base address of the guest-visible v2m MSI frame.
133    #[cfg_attr(feature = "inspect", inspect(hex))]
134    pub frame_base: u64,
135    /// First GIC interrupt ID in the SPI range owned by this frame.
136    pub spi_base: u32,
137    /// Number of SPIs owned by this frame.
138    pub spi_count: u32,
139}
140
141/// GICv3 ITS (Interrupt Translation Service) parameters.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
144pub struct GicItsInfo {
145    /// Physical base address of the ITS MMIO region (must be 64 KiB aligned).
146    #[cfg_attr(feature = "inspect", inspect(hex))]
147    pub its_base: u64,
148}
149
150/// MSI controller configuration for PCIe interrupt delivery.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
153#[cfg_attr(feature = "inspect", inspect(external_tag))]
154pub enum GicMsiController {
155    /// No MSI controller configured.
156    None,
157    /// GICv2m — maps MSI writes to a fixed pool of SPIs.
158    V2m(GicV2mInfo),
159    /// GICv3 ITS — routes MSIs via LPIs using (DeviceID, EventID) lookup.
160    Its(GicItsInfo),
161}
162
163/// ARM64 specific VP info.
164#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
165#[derive(Debug, Copy, Clone)]
166pub struct Aarch64VpInfo {
167    /// The base info.
168    #[cfg_attr(feature = "inspect", inspect(flatten))]
169    pub base: VpInfo,
170    /// The MPIDR_EL1 value of the processor.
171    #[cfg_attr(feature = "inspect", inspect(hex, with = "|&x| u64::from(x)"))]
172    pub mpidr: MpidrEl1,
173    /// GIC Redistributor Address (GICv3 only; `None` for GICv2).
174    #[cfg_attr(feature = "inspect", inspect(hex))]
175    pub gicr: Option<u64>,
176    /// Performance Interrupt GSIV (PMU)
177    #[cfg_attr(feature = "inspect", inspect(hex))]
178    pub pmu_gsiv: Option<u32>,
179}
180
181impl AsRef<VpInfo> for Aarch64VpInfo {
182    fn as_ref(&self) -> &VpInfo {
183        &self.base
184    }
185}
186
187impl AsMut<VpInfo> for Aarch64VpInfo {
188    fn as_mut(&mut self) -> &mut VpInfo {
189        &mut self.base
190    }
191}
192
193impl TopologyBuilder<Aarch64Topology> {
194    /// Returns a builder for creating an aarch64 processor topology.
195    pub fn new_aarch64(platform: Aarch64PlatformConfig) -> Self {
196        Self {
197            vps_per_socket: 1,
198            smt_enabled: false,
199            arch: Aarch64TopologyBuilderState { platform },
200        }
201    }
202
203    /// Builds a processor topology with `proc_count` processors.
204    pub fn build(
205        &self,
206        proc_count: u32,
207    ) -> Result<ProcessorTopology<Aarch64Topology>, InvalidTopology> {
208        if proc_count >= 256 {
209            return Err(InvalidTopology::TooManyVps {
210                requested: proc_count,
211                max: u8::MAX.into(),
212            });
213        }
214        if let GicVersion::V2 { .. } = self.arch.platform.gic_version {
215            if proc_count > 8 {
216                return Err(InvalidTopology::TooManyCpusForGicV2(proc_count));
217            }
218        }
219        if !(16..32).contains(&self.arch.platform.virt_timer_ppi) {
220            return Err(InvalidTopology::InvalidPpiIntid(
221                self.arch.platform.virt_timer_ppi,
222            ));
223        }
224        if let Some(gsiv) = self.arch.platform.pmu_gsiv {
225            if !(16..32).contains(&gsiv) {
226                return Err(InvalidTopology::InvalidPpiIntid(gsiv));
227            }
228        }
229        let nr = self.arch.platform.gic_nr_irqs;
230        if !(64..=992).contains(&nr) || !nr.is_multiple_of(32) {
231            return Err(InvalidTopology::InvalidGicNrIrqs(nr));
232        }
233        let smt_enabled = self.effective_smt();
234        let uni_proc = proc_count == 1;
235        let mpidrs = (0..proc_count).map(|vp_index| {
236            let mpidr = if smt_enabled {
237                smt_mpidr(vp_index)
238            } else {
239                non_smt_mpidr(vp_index)
240            };
241            mpidr.with_res1_31(true).with_u(uni_proc)
242        });
243        let gic_version = self.arch.platform.gic_version;
244        self.build_with_vp_info(mpidrs.enumerate().map(move |(id, mpidr)| {
245            // GICv3 assigns a per-VP redistributor region; GICv2 has no
246            // redistributors so the field is zero.
247            let gicr = match gic_version {
248                GicVersion::V3 {
249                    redistributors_base,
250                } => Some(redistributors_base + id as u64 * aarch64defs::GIC_REDISTRIBUTOR_SIZE),
251                GicVersion::V2 { .. } => None,
252            };
253            Aarch64VpInfo {
254                base: VpInfo {
255                    vp_index: VpIndex::new(id as u32),
256                    vnode: id as u32 / self.vps_per_socket,
257                },
258                mpidr,
259                gicr,
260                pmu_gsiv: self.arch.platform.pmu_gsiv,
261            }
262        }))
263    }
264
265    /// Returns whether SMT applies to this configuration.
266    ///
267    /// A socket holding a single VP has no sibling to pair with, so requesting
268    /// SMT there would claim a thread of a core that does not exist. x86 clamps
269    /// the same way.
270    fn effective_smt(&self) -> bool {
271        self.smt_enabled && self.vps_per_socket > 1
272    }
273
274    /// Builds a processor topology with processors with the specified information.
275    ///
276    /// The MPIDRs are taken as given; they are not checked against the socket
277    /// and SMT configuration, and nothing derives topology from them. Logical
278    /// topology comes from `vps_per_socket` and `smt_enabled`, so a caller that
279    /// supplies VPs arranged some other way will have that arrangement
280    /// reported as whatever it declared through the builder.
281    pub fn build_with_vp_info(
282        &self,
283        vps: impl IntoIterator<Item = Aarch64VpInfo>,
284    ) -> Result<ProcessorTopology<Aarch64Topology>, InvalidTopology> {
285        let vps = Vec::from_iter(vps);
286        for (i, vp) in vps.iter().enumerate() {
287            if i != vp.base.vp_index.index() as usize {
288                return Err(InvalidTopology::InvalidVpIndices);
289            }
290        }
291
292        Ok(ProcessorTopology {
293            vps,
294            smt_enabled: self.effective_smt(),
295            vps_per_socket: self.vps_per_socket,
296            arch: Aarch64Topology {
297                platform: self.arch.platform,
298            },
299        })
300    }
301}
302
303impl ProcessorTopology<Aarch64Topology> {
304    /// Returns the GIC version and version-specific addresses.
305    pub fn gic_version(&self) -> GicVersion {
306        self.arch.platform.gic_version
307    }
308
309    /// Returns the GIC distributor base
310    pub fn gic_distributor_base(&self) -> u64 {
311        self.arch.platform.gic_distributor_base
312    }
313
314    /// Returns the PMU GSIV
315    pub fn pmu_gsiv(&self) -> Option<u32> {
316        self.arch.platform.pmu_gsiv
317    }
318
319    /// Returns the MSI controller configuration.
320    pub fn gic_msi(&self) -> GicMsiController {
321        self.arch.platform.gic_msi
322    }
323
324    /// Returns the virtual timer PPI (GIC INTID).
325    pub fn virt_timer_ppi(&self) -> u32 {
326        self.arch.platform.virt_timer_ppi
327    }
328
329    /// Returns the total number of GIC interrupts to configure.
330    pub fn gic_nr_irqs(&self) -> u32 {
331        self.arch.platform.gic_nr_irqs
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    fn platform() -> Aarch64PlatformConfig {
340        Aarch64PlatformConfig {
341            gic_distributor_base: 0xffff0000,
342            gic_version: GicVersion::V3 {
343                redistributors_base: 0xefff0000,
344            },
345            gic_msi: GicMsiController::None,
346            pmu_gsiv: None,
347            virt_timer_ppi: 20,
348            gic_nr_irqs: 992,
349        }
350    }
351
352    fn builder() -> TopologyBuilder<Aarch64Topology> {
353        TopologyBuilder::new_aarch64(platform())
354    }
355
356    /// Returns `(mpidr, socket, core, thread)` for each VP.
357    fn describe(topology: &ProcessorTopology<Aarch64Topology>) -> Vec<(u64, u32, u32, u32)> {
358        topology
359            .vps_arch()
360            .map(|vp| {
361                let t = topology.vp_topology(vp.base.vp_index);
362                (vp.mpidr.into(), t.socket, t.core, t.thread)
363            })
364            .collect()
365    }
366
367    /// Strips the RES1 and uniprocessor bits so tests can compare affinities.
368    fn affinity(mpidr: u64) -> u64 {
369        mpidr & (u64::from(MpidrEl1::AFFINITY_MASK) | 1 << 24)
370    }
371
372    /// A socket of one has no sibling to pair with, so SMT is dropped rather
373    /// than claiming a thread of a core that does not exist.
374    #[test]
375    fn single_vp_ignores_smt() {
376        let topology = builder().smt_enabled(true).build(1).unwrap();
377        assert!(!topology.smt_enabled());
378        let mpidr = topology.vp_arch(VpIndex::new(0)).mpidr;
379        assert!(mpidr.u());
380        assert!(!mpidr.mt());
381        assert_eq!(describe(&topology), [(u64::from(mpidr), 0, 0, 0)]);
382    }
383
384    /// Aff0 holds 16 VPs, then rolls into Aff1. A GICv3 targeted SGI cannot
385    /// reach an Aff0 above 15, which is what this boundary exists for.
386    #[test]
387    fn seventeen_vps_roll_into_aff1() {
388        let topology = builder().vps_per_socket(17).build(17).unwrap();
389        let vps = describe(&topology);
390        assert_eq!(affinity(vps[15].0), 0x0f);
391        assert_eq!(affinity(vps[16].0), 0x100);
392        for (i, (mpidr, socket, core, thread)) in vps.into_iter().enumerate() {
393            assert!(MpidrEl1::from(mpidr).aff0() < 16);
394            assert_eq!((socket, core, thread), (0, i as u32, 0));
395        }
396    }
397
398    /// Logical topology follows `vps_per_socket`; MPIDRs keep packing linearly
399    /// and say nothing about sockets.
400    #[test]
401    fn multiple_sockets_without_smt() {
402        let topology = builder().vps_per_socket(4).build(8).unwrap();
403        let vps = describe(&topology);
404        for (i, (mpidr, socket, core, thread)) in vps.iter().copied().enumerate() {
405            assert_eq!(affinity(mpidr), i as u64);
406            assert_eq!((socket, core, thread), (i as u32 / 4, i as u32 % 4, 0));
407        }
408        let vnodes: Vec<_> = topology.vps().map(|vp| vp.vnode).collect();
409        assert_eq!(vnodes, [0, 0, 0, 0, 1, 1, 1, 1]);
410    }
411
412    /// The exact register values a guest sees, RES1 and MT bits included,
413    /// rather than just the affinity fields.
414    #[test]
415    fn mpidr_register_values() {
416        for (vp, expected) in [
417            (0, 0x8000_0000),
418            (1, 0x8000_0001),
419            (15, 0x8000_000f),
420            (16, 0x8000_0100),
421            (17, 0x8000_0101),
422        ] {
423            assert_eq!(
424                u64::from(non_smt_mpidr(vp).with_res1_31(true)),
425                expected,
426                "non-SMT VP {vp}"
427            );
428        }
429
430        for (vp, expected) in [
431            (0, 0x8100_0000),
432            (1, 0x8100_0001),
433            (2, 0x8100_0100),
434            (3, 0x8100_0101),
435        ] {
436            assert_eq!(
437                u64::from(smt_mpidr(vp).with_res1_31(true)),
438                expected,
439                "SMT VP {vp}"
440            );
441        }
442    }
443
444    /// Sockets do not appear in the MPIDR: the core index keeps packing past
445    /// the socket boundary, and only the logical topology splits there.
446    #[test]
447    fn smt_sockets_do_not_change_affinity() {
448        let topology = builder()
449            .vps_per_socket(2)
450            .smt_enabled(true)
451            .build(4)
452            .unwrap();
453        assert!(topology.smt_enabled());
454        assert_eq!(
455            describe(&topology)
456                .into_iter()
457                .map(|(mpidr, socket, core, thread)| (affinity(mpidr), socket, core, thread))
458                .collect::<Vec<_>>(),
459            [
460                (0x0100_0000, 0, 0, 0),
461                (0x0100_0001, 0, 0, 1),
462                (0x0100_0100, 1, 0, 0),
463                (0x0100_0101, 1, 0, 1),
464            ]
465        );
466    }
467
468    /// An odd socket size just leaves the last core with one thread. The MPIDRs
469    /// stay unique, so there is nothing to reject.
470    ///
471    /// MPIDR pairs threads across the whole VM while the logical topology pairs
472    /// them within a socket, so the two disagree once a socket holds an odd
473    /// number of VPs. PPTT is what describes topology, so that is tolerable.
474    #[test]
475    fn odd_socket_size_under_smt() {
476        let topology = builder()
477            .vps_per_socket(3)
478            .smt_enabled(true)
479            .build(3)
480            .unwrap();
481        assert_eq!(
482            describe(&topology)
483                .into_iter()
484                .map(|(_, socket, core, thread)| (socket, core, thread))
485                .collect::<Vec<_>>(),
486            [(0, 0, 0), (0, 0, 1), (0, 1, 0)]
487        );
488    }
489
490    /// OpenHCL passes host MPIDRs through unmodified, including ones this code
491    /// would never generate.
492    #[test]
493    fn caller_supplied_mpidrs_are_preserved() {
494        let mpidrs = [0x81, 0x40, 0x0];
495        let topology = builder()
496            .vps_per_socket(3)
497            .build_with_vp_info(mpidrs.iter().enumerate().map(|(i, &mpidr)| Aarch64VpInfo {
498                base: VpInfo {
499                    vp_index: VpIndex::new(i as u32),
500                    vnode: 0,
501                },
502                mpidr: MpidrEl1::from(mpidr),
503                gicr: None,
504                pmu_gsiv: None,
505            }))
506            .unwrap();
507
508        assert_eq!(
509            topology
510                .vps_arch()
511                .map(|vp| u64::from(vp.mpidr))
512                .collect::<Vec<_>>(),
513            mpidrs
514        );
515        // Topology still comes from the VP index, not from the odd affinities.
516        assert_eq!(
517            describe(&topology)
518                .into_iter()
519                .map(|(_, socket, core, thread)| (socket, core, thread))
520                .collect::<Vec<_>>(),
521            [(0, 0, 0), (0, 1, 0), (0, 2, 0)]
522        );
523    }
524
525    #[test]
526    fn gicv2_vp_limit() {
527        let gicv2 = || {
528            TopologyBuilder::new_aarch64(Aarch64PlatformConfig {
529                gic_version: GicVersion::V2 {
530                    cpu_interface_base: 0xefff0000,
531                },
532                ..platform()
533            })
534        };
535        assert!(gicv2().vps_per_socket(8).build(8).is_ok());
536        assert!(matches!(
537            gicv2().vps_per_socket(9).build(9),
538            Err(InvalidTopology::TooManyCpusForGicV2(9))
539        ));
540    }
541}