Skip to main content

vm_topology/
memory.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Tools to the compute guest memory layout.
5
6use memory_range::MemoryRange;
7use memory_range::subtract_ranges;
8use thiserror::Error;
9
10const PAGE_SIZE: u64 = 4096;
11const FOUR_GB: u64 = 0x1_0000_0000;
12
13/// Represents a page-aligned byte range of memory, with additional metadata.
14#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
15#[cfg_attr(feature = "mesh", derive(mesh_protobuf::Protobuf))]
16#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
17pub struct MemoryRangeWithNode {
18    /// The memory range.
19    pub range: MemoryRange,
20    /// The virtual NUMA node the range belongs to.
21    pub vnode: u32,
22}
23
24impl core::fmt::Display for MemoryRangeWithNode {
25    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26        write!(f, "{}({})", self.range, self.vnode)
27    }
28}
29
30/// Describes the memory layout of a guest.
31#[derive(Debug, Clone)]
32#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
33pub struct MemoryLayout {
34    #[cfg_attr(feature = "inspect", inspect(with = "inspect_ranges_with_metadata"))]
35    ram: Vec<MemoryRangeWithNode>,
36    #[cfg_attr(feature = "inspect", inspect(with = "inspect_ranges"))]
37    mmio: Vec<MemoryRange>,
38    #[cfg_attr(feature = "inspect", inspect(with = "inspect_ranges"))]
39    pci_ecam: Vec<MemoryRange>,
40    #[cfg_attr(feature = "inspect", inspect(with = "inspect_ranges"))]
41    pci_mmio: Vec<MemoryRange>,
42    /// The RAM range used by VTL2. This is not present in any of the stats
43    /// above.
44    vtl2_range: Option<MemoryRange>,
45}
46
47#[cfg(feature = "inspect")]
48fn inspect_ranges(ranges: &[MemoryRange]) -> impl '_ + inspect::Inspect {
49    inspect::iter_by_key(ranges.iter().map(|range| {
50        (
51            range.to_string(),
52            inspect::adhoc(|i| {
53                i.respond().hex("length", range.len());
54            }),
55        )
56    }))
57}
58
59#[cfg(feature = "inspect")]
60fn inspect_ranges_with_metadata(ranges: &[MemoryRangeWithNode]) -> impl '_ + inspect::Inspect {
61    inspect::iter_by_key(ranges.iter().map(|range| {
62        (
63            range.range.to_string(),
64            inspect::adhoc(|i| {
65                i.respond()
66                    .hex("length", range.range.len())
67                    .hex("vnode", range.vnode);
68            }),
69        )
70    }))
71}
72
73/// Memory layout creation error.
74#[derive(Debug, Error)]
75pub enum Error {
76    /// Invalid memory size.
77    #[error("invalid memory size")]
78    BadSize,
79    /// Invalid per-NUMA-node memory size.
80    #[error("invalid NUMA node memory size")]
81    BadNumaSize,
82    /// Empty NUMA memory sizes.
83    #[error("empty NUMA memory sizes")]
84    EmptyNumaSizes,
85    /// Invalid MMIO gap configuration.
86    #[error("invalid MMIO gap configuration")]
87    BadMmioGaps,
88    /// Invalid memory ranges.
89    #[error("invalid memory or MMIO ranges")]
90    BadMemoryRanges,
91    /// VTL2 range is below the end of ram, and overlaps.
92    #[error("vtl2 range is below end of ram")]
93    Vtl2RangeBeforeEndOfRam,
94}
95
96fn validate_ranges(ranges: &[MemoryRange]) -> Result<(), Error> {
97    validate_ranges_core(ranges, |x| x)
98}
99
100fn validate_ranges_with_metadata(ranges: &[MemoryRangeWithNode]) -> Result<(), Error> {
101    validate_ranges_core(ranges, |x| &x.range)
102}
103
104/// Ensures everything in a list of ranges is non-empty, in order, and
105/// non-overlapping.
106fn validate_ranges_core<T>(ranges: &[T], getter: impl Fn(&T) -> &MemoryRange) -> Result<(), Error> {
107    if ranges.iter().any(|x| getter(x).is_empty())
108        || !ranges.iter().zip(ranges.iter().skip(1)).all(|(x, y)| {
109            let x = getter(x);
110            let y = getter(y);
111            x <= y && !x.overlaps(y)
112        })
113    {
114        return Err(Error::BadMemoryRanges);
115    }
116
117    Ok(())
118}
119
120/// The type backing an address.
121#[derive(Debug, Copy, Clone, PartialEq, Eq)]
122pub enum AddressType {
123    /// The address describes ram.
124    Ram,
125    /// The address describes mmio.
126    Mmio,
127    /// The address describes PCI ECAM.
128    PciEcam,
129    /// The address describes PCI MMIO.
130    PciMmio,
131}
132
133impl MemoryLayout {
134    /// Makes a new memory layout for a guest with `ram_size` bytes of memory
135    /// and MMIO gaps at the locations specified by `gaps`.
136    ///
137    /// `ram_size` must be a multiple of the page size. Each mmio and device
138    /// reserved gap must be non-empty, and the gaps must be in order and
139    /// non-overlapping.
140    ///
141    /// `vtl2_range` describes a range of memory reserved for VTL2.
142    /// It is not reported in ram.
143    ///
144    /// All RAM is assigned to NUMA node 0.
145    pub fn new(
146        ram_size: u64,
147        mmio_gaps: &[MemoryRange],
148        pci_ecam_gaps: &[MemoryRange],
149        pci_mmio_gaps: &[MemoryRange],
150        vtl2_range: Option<MemoryRange>,
151    ) -> Result<Self, Error> {
152        if ram_size == 0 || ram_size & (PAGE_SIZE - 1) != 0 {
153            return Err(Error::BadSize);
154        }
155        Self::new_with_numa(
156            &[ram_size],
157            mmio_gaps,
158            pci_ecam_gaps,
159            pci_mmio_gaps,
160            vtl2_range,
161        )
162    }
163
164    /// Like [`Self::new()`], but distributes RAM across NUMA nodes according
165    /// to the per-node sizes in `numa_mem_sizes`.
166    ///
167    /// `numa_mem_sizes[i]` is the number of RAM bytes for vnode `i`.
168    /// Each size must be page-aligned and non-zero. The sum of all sizes
169    /// is the total guest RAM.
170    ///
171    /// RAM is placed sequentially around MMIO gaps, filling each node's
172    /// budget in order. When a node's budget is exhausted mid-chunk,
173    /// the chunk is split and the next node continues from that address.
174    pub fn new_with_numa(
175        numa_mem_sizes: &[u64],
176        mmio_gaps: &[MemoryRange],
177        pci_ecam_gaps: &[MemoryRange],
178        pci_mmio_gaps: &[MemoryRange],
179        vtl2_range: Option<MemoryRange>,
180    ) -> Result<Self, Error> {
181        if numa_mem_sizes.is_empty() {
182            return Err(Error::EmptyNumaSizes);
183        }
184
185        for &size in numa_mem_sizes {
186            if size == 0 || size & (PAGE_SIZE - 1) != 0 {
187                return Err(Error::BadNumaSize);
188            }
189        }
190
191        let ram_size: u64 = numa_mem_sizes
192            .iter()
193            .try_fold(0u64, |acc, &s| acc.checked_add(s))
194            .ok_or(Error::BadSize)?;
195
196        validate_ranges(mmio_gaps)?;
197        validate_ranges(pci_ecam_gaps)?;
198        validate_ranges(pci_mmio_gaps)?;
199
200        let mut combined_gaps = mmio_gaps
201            .iter()
202            .chain(pci_ecam_gaps)
203            .chain(pci_mmio_gaps)
204            .copied()
205            .collect::<Vec<_>>();
206        combined_gaps.sort();
207        validate_ranges(&combined_gaps)?;
208
209        let available = subtract_ranges(
210            [MemoryRange::new(0..MemoryRange::MAX_ADDRESS)],
211            combined_gaps,
212        );
213
214        // Distribute RAM across NUMA nodes, filling available ranges in order.
215        let mut ram = Vec::new();
216        let mut remaining = ram_size;
217        let mut node_idx = 0;
218        let mut node_remaining = numa_mem_sizes[0];
219
220        for range in available {
221            let range_size = remaining.min(range.len());
222            let mut offset = 0;
223
224            while offset < range_size {
225                if node_remaining == 0 {
226                    node_idx += 1;
227                    node_remaining = *numa_mem_sizes
228                        .get(node_idx)
229                        .expect("node budget exhausted before all RAM placed");
230                }
231
232                let piece = (range_size - offset).min(node_remaining);
233                let start = range.start() + offset;
234                ram.push(MemoryRangeWithNode {
235                    range: MemoryRange::new(start..start + piece),
236                    vnode: node_idx as u32,
237                });
238                offset += piece;
239                node_remaining -= piece;
240            }
241
242            remaining -= range_size;
243
244            if remaining == 0 {
245                break;
246            }
247        }
248
249        Self::build(
250            ram,
251            mmio_gaps.to_vec(),
252            pci_ecam_gaps.to_vec(),
253            pci_mmio_gaps.to_vec(),
254            vtl2_range,
255        )
256    }
257
258    /// Makes a new memory layout for a guest with the given mmio gaps and
259    /// memory ranges.
260    ///
261    /// `memory` and `gaps` ranges must be in sorted order and non-overlapping,
262    /// and describe page aligned ranges.
263    pub fn new_from_ranges(
264        memory: &[MemoryRangeWithNode],
265        gaps: &[MemoryRange],
266    ) -> Result<Self, Error> {
267        validate_ranges_with_metadata(memory)?;
268        validate_ranges(gaps)?;
269        Self::build(memory.to_vec(), gaps.to_vec(), vec![], vec![], None)
270    }
271
272    /// Makes a new memory layout from already-resolved RAM and fixed ranges.
273    ///
274    /// Each individual range must be non-empty, but the lists themselves may
275    /// be empty (e.g. no PCIe root complexes means empty PCI ECAM/MMIO
276    /// vectors). Ranges within each list must be sorted and non-overlapping.
277    /// MMIO gaps may contain empty placeholder ranges to preserve positional
278    /// indexing (e.g. `mmio()[0]` = low, `mmio()[1]` = high); empty entries
279    /// are ignored during validation. The combined layout is also validated
280    /// for overlaps, including the optional VTL2 range.
281    pub fn new_from_resolved_ranges(
282        ram: Vec<MemoryRangeWithNode>,
283        mmio_gaps: Vec<MemoryRange>,
284        pci_ecam_gaps: Vec<MemoryRange>,
285        pci_mmio_gaps: Vec<MemoryRange>,
286        vtl2_range: Option<MemoryRange>,
287    ) -> Result<Self, Error> {
288        validate_ranges_with_metadata(&ram)?;
289        // MMIO gaps may include empty placeholders for positional indexing;
290        // validate only the non-empty entries.
291        let non_empty_mmio: Vec<_> = mmio_gaps
292            .iter()
293            .copied()
294            .filter(|r| !r.is_empty())
295            .collect();
296        validate_ranges(&non_empty_mmio)?;
297        validate_ranges(&pci_ecam_gaps)?;
298        validate_ranges(&pci_mmio_gaps)?;
299
300        Self::build(ram, mmio_gaps, pci_ecam_gaps, pci_mmio_gaps, vtl2_range)
301    }
302
303    /// Builds the memory layout.
304    ///
305    /// `ram` must already be known to be sorted.
306    fn build(
307        ram: Vec<MemoryRangeWithNode>,
308        mmio: Vec<MemoryRange>,
309        pci_ecam: Vec<MemoryRange>,
310        pci_mmio: Vec<MemoryRange>,
311        vtl2_range: Option<MemoryRange>,
312    ) -> Result<Self, Error> {
313        // Filter out empty placeholder ranges before validation and overlap
314        // checks — they carry no physical meaning and exist only for
315        // positional indexing in the stored mmio vector.
316        let mut all_ranges = ram
317            .iter()
318            .map(|x| &x.range)
319            .chain(&mmio)
320            .chain(&vtl2_range)
321            .chain(&pci_ecam)
322            .chain(&pci_mmio)
323            .copied()
324            .filter(|r| !r.is_empty())
325            .collect::<Vec<_>>();
326
327        all_ranges.sort();
328        validate_ranges(&all_ranges)?;
329
330        if all_ranges
331            .iter()
332            .zip(all_ranges.iter().skip(1))
333            .any(|(x, y)| x.overlaps(y))
334        {
335            return Err(Error::BadMemoryRanges);
336        }
337
338        let last_ram_entry = ram.last().ok_or(Error::BadMemoryRanges)?;
339        let end_of_ram = last_ram_entry.range.end();
340
341        if let Some(range) = vtl2_range {
342            if range.start() < end_of_ram {
343                return Err(Error::Vtl2RangeBeforeEndOfRam);
344            }
345        }
346
347        Ok(Self {
348            ram,
349            mmio,
350            pci_ecam,
351            pci_mmio,
352            vtl2_range,
353        })
354    }
355
356    /// The MMIO gap ranges.
357    pub fn mmio(&self) -> &[MemoryRange] {
358        &self.mmio
359    }
360
361    /// The populated RAM ranges. This does not include the vtl2_range.
362    pub fn ram(&self) -> &[MemoryRangeWithNode] {
363        &self.ram
364    }
365
366    /// A special memory range for VTL2, if any. This memory range is treated
367    /// like RAM, but is only used to hold VTL2 and is located above ram and
368    /// mmio.
369    pub fn vtl2_range(&self) -> Option<MemoryRange> {
370        self.vtl2_range
371    }
372
373    /// The total RAM size in bytes. This is not contiguous.
374    pub fn ram_size(&self) -> u64 {
375        self.ram.iter().map(|r| r.range.len()).sum()
376    }
377
378    /// One past the last byte of RAM.
379    pub fn end_of_ram(&self) -> u64 {
380        // always at least one RAM range
381        self.ram.last().expect("mmio set").range.end()
382    }
383
384    /// The bytes of RAM below 4GB.
385    pub fn ram_below_4gb(&self) -> u64 {
386        self.ram
387            .iter()
388            .filter(|r| r.range.end() < FOUR_GB)
389            .map(|r| r.range.len())
390            .sum()
391    }
392
393    /// The bytes of RAM at or above 4GB.
394    pub fn ram_above_4gb(&self) -> u64 {
395        self.ram
396            .iter()
397            .filter(|r| r.range.end() >= FOUR_GB)
398            .map(|r| r.range.len())
399            .sum()
400    }
401
402    /// The bytes of RAM above the high MMIO gap.
403    ///
404    /// Returns None if there aren't exactly 2 MMIO gaps.
405    pub fn ram_above_high_mmio(&self) -> Option<u64> {
406        if self.mmio.len() != 2 {
407            return None;
408        }
409
410        Some(
411            self.ram
412                .iter()
413                .filter(|r| r.range.start() >= self.mmio[1].end())
414                .map(|r| r.range.len())
415                .sum(),
416        )
417    }
418
419    /// The ending RAM address below 4GB.
420    ///
421    /// Returns None if there is no RAM mapped below 4GB.
422    pub fn max_ram_below_4gb(&self) -> Option<u64> {
423        Some(
424            self.ram
425                .iter()
426                .rev()
427                .find(|r| r.range.end() < FOUR_GB)?
428                .range
429                .end(),
430        )
431    }
432
433    /// One past the last byte of RAM, MMIO, PCI ECAM, or PCI MMIO.
434    pub fn end_of_layout(&self) -> u64 {
435        [
436            self.mmio
437                .iter()
438                .filter(|r| !r.is_empty())
439                .map(|r| r.end())
440                .max()
441                .unwrap_or(0),
442            self.end_of_ram(),
443            self.pci_ecam.last().map(|r| r.end()).unwrap_or(0),
444            self.pci_mmio.last().map(|r| r.end()).unwrap_or(0),
445        ]
446        .into_iter()
447        .max()
448        .unwrap()
449    }
450
451    /// Probe a given address to see if it is in the memory layout described by
452    /// `self`. Returns the [`AddressType`] of the address if it is in the
453    /// layout.
454    ///
455    /// This does not check the vtl2_range.
456    pub fn probe_address(&self, address: u64) -> Option<AddressType> {
457        let ranges = self
458            .ram
459            .iter()
460            .map(|r| (&r.range, AddressType::Ram))
461            .chain(self.mmio.iter().map(|r| (r, AddressType::Mmio)))
462            .chain(self.pci_ecam.iter().map(|r| (r, AddressType::PciEcam)))
463            .chain(self.pci_mmio.iter().map(|r| (r, AddressType::PciMmio)));
464
465        for (range, address_type) in ranges {
466            if range.contains_addr(address) {
467                return Some(address_type);
468            }
469        }
470
471        None
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    const KB: u64 = 1024;
480    const MB: u64 = 1024 * KB;
481    const GB: u64 = 1024 * MB;
482    const TB: u64 = 1024 * GB;
483
484    #[test]
485    fn layout() {
486        let mmio = &[
487            MemoryRange::new(GB..2 * GB),
488            MemoryRange::new(3 * GB..4 * GB),
489        ];
490        let ram = &[
491            MemoryRangeWithNode {
492                range: MemoryRange::new(0..GB),
493                vnode: 0,
494            },
495            MemoryRangeWithNode {
496                range: MemoryRange::new(2 * GB..3 * GB),
497                vnode: 0,
498            },
499            MemoryRangeWithNode {
500                range: MemoryRange::new(4 * GB..TB + 2 * GB),
501                vnode: 0,
502            },
503        ];
504
505        let layout = MemoryLayout::new(TB, mmio, &[], &[], None).unwrap();
506        assert_eq!(
507            layout.ram(),
508            &[
509                MemoryRangeWithNode {
510                    range: MemoryRange::new(0..GB),
511                    vnode: 0
512                },
513                MemoryRangeWithNode {
514                    range: MemoryRange::new(2 * GB..3 * GB),
515                    vnode: 0
516                },
517                MemoryRangeWithNode {
518                    range: MemoryRange::new(4 * GB..TB + 2 * GB),
519                    vnode: 0
520                },
521            ]
522        );
523        assert_eq!(layout.mmio(), mmio);
524        assert_eq!(layout.ram_size(), TB);
525        assert_eq!(layout.end_of_ram(), TB + 2 * GB);
526        assert_eq!(layout.end_of_layout(), TB + 2 * GB);
527
528        let layout = MemoryLayout::new_from_ranges(ram, mmio).unwrap();
529        assert_eq!(
530            layout.ram(),
531            &[
532                MemoryRangeWithNode {
533                    range: MemoryRange::new(0..GB),
534                    vnode: 0
535                },
536                MemoryRangeWithNode {
537                    range: MemoryRange::new(2 * GB..3 * GB),
538                    vnode: 0
539                },
540                MemoryRangeWithNode {
541                    range: MemoryRange::new(4 * GB..TB + 2 * GB),
542                    vnode: 0
543                },
544            ]
545        );
546        assert_eq!(layout.mmio(), mmio);
547        assert_eq!(layout.ram_size(), TB);
548        assert_eq!(layout.end_of_ram(), TB + 2 * GB);
549        assert_eq!(layout.end_of_layout(), TB + 2 * GB);
550    }
551
552    #[test]
553    fn bad_layout() {
554        MemoryLayout::new(TB + 1, &[], &[], &[], None).unwrap_err();
555        let mmio = &[
556            MemoryRange::new(3 * GB..4 * GB),
557            MemoryRange::new(GB..2 * GB),
558        ];
559        MemoryLayout::new(TB, mmio, &[], &[], None).unwrap_err();
560
561        MemoryLayout::new_from_ranges(&[], mmio).unwrap_err();
562
563        let ram = &[MemoryRangeWithNode {
564            range: MemoryRange::new(0..GB),
565            vnode: 0,
566        }];
567        MemoryLayout::new_from_ranges(ram, mmio).unwrap_err();
568
569        let ram = &[MemoryRangeWithNode {
570            range: MemoryRange::new(0..GB + MB),
571            vnode: 0,
572        }];
573        let mmio = &[
574            MemoryRange::new(GB..2 * GB),
575            MemoryRange::new(3 * GB..4 * GB),
576        ];
577        MemoryLayout::new_from_ranges(ram, mmio).unwrap_err();
578
579        let mmio = &[
580            MemoryRange::new(GB..2 * GB),
581            MemoryRange::new(3 * GB..4 * GB),
582        ];
583        let pci_ecam = &[MemoryRange::new(GB..GB + MB)];
584        MemoryLayout::new(TB, mmio, pci_ecam, &[], None).unwrap_err();
585
586        let mmio = &[
587            MemoryRange::new(GB..2 * GB),
588            MemoryRange::new(3 * GB..4 * GB),
589        ];
590        let pci_mmio = &[MemoryRange::new(GB..GB + MB)];
591        MemoryLayout::new(TB, mmio, &[], pci_mmio, None).unwrap_err();
592
593        let pci_ecam = &[MemoryRange::new(GB..GB + MB)];
594        let pci_mmio = &[MemoryRange::new(GB..GB + MB)];
595        MemoryLayout::new(TB, &[], pci_ecam, pci_mmio, None).unwrap_err();
596    }
597
598    #[test]
599    fn resolved_ranges_constructor() {
600        let ram = vec![
601            MemoryRangeWithNode {
602                range: MemoryRange::new(0..GB),
603                vnode: 0,
604            },
605            MemoryRangeWithNode {
606                range: MemoryRange::new(2 * GB..3 * GB),
607                vnode: 1,
608            },
609        ];
610        let mmio = vec![MemoryRange::new(GB..2 * GB)];
611        let pci_ecam = vec![MemoryRange::new(4 * GB..4 * GB + MB)];
612        let pci_mmio = vec![MemoryRange::new(5 * GB..6 * GB)];
613
614        let layout = MemoryLayout::new_from_resolved_ranges(
615            ram.clone(),
616            mmio.clone(),
617            pci_ecam.clone(),
618            pci_mmio.clone(),
619            None,
620        )
621        .unwrap();
622
623        assert_eq!(layout.ram(), ram);
624        assert_eq!(layout.mmio(), mmio);
625        assert_eq!(layout.probe_address(4 * GB), Some(AddressType::PciEcam));
626        assert_eq!(layout.probe_address(5 * GB), Some(AddressType::PciMmio));
627    }
628
629    #[test]
630    fn resolved_ranges_reject_overlap_with_fixed_ranges() {
631        let ram = vec![MemoryRangeWithNode {
632            range: MemoryRange::new(0..2 * GB),
633            vnode: 0,
634        }];
635        let mmio = vec![MemoryRange::new(GB..2 * GB)];
636
637        assert!(MemoryLayout::new_from_resolved_ranges(ram, mmio, vec![], vec![], None).is_err());
638    }
639
640    #[test]
641    fn resolved_ranges_validate_vtl2_against_ram_end() {
642        let ram = vec![
643            MemoryRangeWithNode {
644                range: MemoryRange::new(0..GB),
645                vnode: 0,
646            },
647            MemoryRangeWithNode {
648                range: MemoryRange::new(3 * GB..4 * GB),
649                vnode: 0,
650            },
651        ];
652        let mmio = vec![MemoryRange::new(GB..2 * GB)];
653        let vtl2_range = MemoryRange::new(2 * GB..2 * GB + MB);
654
655        assert!(matches!(
656            MemoryLayout::new_from_resolved_ranges(ram, mmio, vec![], vec![], Some(vtl2_range)),
657            Err(Error::Vtl2RangeBeforeEndOfRam)
658        ));
659    }
660
661    #[test]
662    fn pci_ranges() {
663        let mmio = &[MemoryRange::new(3 * GB..4 * GB)];
664        let pci_ecam = &[MemoryRange::new(2 * TB - GB..2 * TB)];
665        let pci_mmio = &[
666            MemoryRange::new(2 * GB..3 * GB),
667            MemoryRange::new(5 * GB..6 * GB),
668        ];
669
670        let layout = MemoryLayout::new(TB, mmio, pci_ecam, pci_mmio, None).unwrap();
671        assert_eq!(
672            layout.ram(),
673            &[
674                MemoryRangeWithNode {
675                    range: MemoryRange::new(0..2 * GB),
676                    vnode: 0,
677                },
678                MemoryRangeWithNode {
679                    range: MemoryRange::new(4 * GB..5 * GB),
680                    vnode: 0,
681                },
682                MemoryRangeWithNode {
683                    range: MemoryRange::new(6 * GB..TB + 3 * GB),
684                    vnode: 0,
685                },
686            ]
687        );
688        assert_eq!(layout.end_of_layout(), 2 * TB);
689
690        assert_eq!(layout.probe_address(2 * GB), Some(AddressType::PciMmio));
691        assert_eq!(
692            layout.probe_address(2 * GB + MB),
693            Some(AddressType::PciMmio)
694        );
695        assert_eq!(layout.probe_address(5 * GB), Some(AddressType::PciMmio));
696        assert_eq!(
697            layout.probe_address(5 * GB + MB),
698            Some(AddressType::PciMmio)
699        );
700        assert_eq!(
701            layout.probe_address(2 * TB - GB),
702            Some(AddressType::PciEcam)
703        );
704    }
705
706    #[test]
707    fn probe_address() {
708        let mmio = &[
709            MemoryRange::new(GB..2 * GB),
710            MemoryRange::new(3 * GB..4 * GB),
711        ];
712        let ram = &[
713            MemoryRangeWithNode {
714                range: MemoryRange::new(0..GB),
715                vnode: 0,
716            },
717            MemoryRangeWithNode {
718                range: MemoryRange::new(2 * GB..3 * GB),
719                vnode: 0,
720            },
721            MemoryRangeWithNode {
722                range: MemoryRange::new(4 * GB..TB + 2 * GB),
723                vnode: 0,
724            },
725        ];
726
727        let layout = MemoryLayout::new_from_ranges(ram, mmio).unwrap();
728
729        assert_eq!(layout.probe_address(0), Some(AddressType::Ram));
730        assert_eq!(layout.probe_address(256), Some(AddressType::Ram));
731        assert_eq!(layout.probe_address(2 * GB), Some(AddressType::Ram));
732        assert_eq!(layout.probe_address(4 * GB), Some(AddressType::Ram));
733        assert_eq!(layout.probe_address(TB), Some(AddressType::Ram));
734        assert_eq!(layout.probe_address(TB + 1), Some(AddressType::Ram));
735
736        assert_eq!(layout.probe_address(GB), Some(AddressType::Mmio));
737        assert_eq!(layout.probe_address(GB + 123), Some(AddressType::Mmio));
738        assert_eq!(layout.probe_address(3 * GB), Some(AddressType::Mmio));
739
740        assert_eq!(layout.probe_address(TB + 2 * GB), None);
741        assert_eq!(layout.probe_address(TB + 3 * GB), None);
742        assert_eq!(layout.probe_address(4 * TB), None);
743    }
744
745    #[test]
746    fn numa_two_nodes_even_split() {
747        // 4 GB total, 2 nodes of 2 GB each, MMIO gap at 2-3 GB.
748        let mmio = &[MemoryRange::new(2 * GB..3 * GB)];
749        let layout = MemoryLayout::new_with_numa(&[2 * GB, 2 * GB], mmio, &[], &[], None).unwrap();
750        assert_eq!(
751            layout.ram(),
752            &[
753                MemoryRangeWithNode {
754                    range: MemoryRange::new(0..2 * GB),
755                    vnode: 0,
756                },
757                MemoryRangeWithNode {
758                    range: MemoryRange::new(3 * GB..5 * GB),
759                    vnode: 1,
760                },
761            ]
762        );
763        assert_eq!(layout.ram_size(), 4 * GB);
764    }
765
766    #[test]
767    fn numa_two_nodes_mid_chunk_split() {
768        // 4 GB total, 2 nodes of 2 GB each, MMIO gap at 3-4 GB.
769        // Node 0's 2 GB fits entirely below the gap; node 1 continues above.
770        // But the first chunk is 3 GB, so node 0 takes 2 GB and node 1
771        // takes the remaining 1 GB of that chunk, plus 1 GB above the gap.
772        let mmio = &[MemoryRange::new(3 * GB..4 * GB)];
773        let layout = MemoryLayout::new_with_numa(&[2 * GB, 2 * GB], mmio, &[], &[], None).unwrap();
774        assert_eq!(
775            layout.ram(),
776            &[
777                MemoryRangeWithNode {
778                    range: MemoryRange::new(0..2 * GB),
779                    vnode: 0,
780                },
781                MemoryRangeWithNode {
782                    range: MemoryRange::new(2 * GB..3 * GB),
783                    vnode: 1,
784                },
785                MemoryRangeWithNode {
786                    range: MemoryRange::new(4 * GB..5 * GB),
787                    vnode: 1,
788                },
789            ]
790        );
791        assert_eq!(layout.ram_size(), 4 * GB);
792    }
793
794    #[test]
795    fn numa_three_nodes() {
796        // 3 GB total, 3 nodes of 1 GB each, no gaps.
797        let layout = MemoryLayout::new_with_numa(&[GB, GB, GB], &[], &[], &[], None).unwrap();
798        assert_eq!(
799            layout.ram(),
800            &[
801                MemoryRangeWithNode {
802                    range: MemoryRange::new(0..GB),
803                    vnode: 0,
804                },
805                MemoryRangeWithNode {
806                    range: MemoryRange::new(GB..2 * GB),
807                    vnode: 1,
808                },
809                MemoryRangeWithNode {
810                    range: MemoryRange::new(2 * GB..3 * GB),
811                    vnode: 2,
812                },
813            ]
814        );
815    }
816
817    #[test]
818    fn numa_single_node_matches_new() {
819        // Single node should produce the same layout as new().
820        let mmio = &[
821            MemoryRange::new(GB..2 * GB),
822            MemoryRange::new(3 * GB..4 * GB),
823        ];
824        let layout_new = MemoryLayout::new(TB, mmio, &[], &[], None).unwrap();
825        let layout_numa = MemoryLayout::new_with_numa(&[TB], mmio, &[], &[], None).unwrap();
826        assert_eq!(layout_new.ram(), layout_numa.ram());
827    }
828
829    #[test]
830    fn numa_bad_inputs() {
831        // Empty sizes.
832        MemoryLayout::new_with_numa(&[], &[], &[], &[], None).unwrap_err();
833        // Non-page-aligned size.
834        MemoryLayout::new_with_numa(&[GB + 1], &[], &[], &[], None).unwrap_err();
835        // Zero size.
836        MemoryLayout::new_with_numa(&[0], &[], &[], &[], None).unwrap_err();
837        // Mixed: one valid, one zero.
838        MemoryLayout::new_with_numa(&[GB, 0], &[], &[], &[], None).unwrap_err();
839    }
840}