#![expect(
unsafe_code,
reason = "the core keeps pointers into the regions, so they're reached through raw pointers only"
)]
use std::ops::Range;
use std::ptr::NonNull;
use crate::emulator::Alias;
use crate::soc::{Endian, Region, Width};
#[derive(Debug)]
struct Bytes {
base: u64,
aliases: Vec<u64>,
writable: bool,
programmable: bool,
erased: u8,
contents: NonNull<u8>,
size: usize,
}
unsafe impl Send for Bytes {}
impl Drop for Bytes {
fn drop(&mut self) {
if self.size > 0 {
unsafe { std::alloc::dealloc(self.contents.as_ptr(), layout(self.size)) }
}
}
}
fn layout(size: usize) -> std::alloc::Layout {
std::alloc::Layout::from_size_align(size, PAGE_ALIGNMENT).expect("a region fits in memory")
}
pub(crate) const PAGE_ALIGNMENT: usize = 4096;
impl Bytes {
fn erased(region: &Region, aliases: Vec<u64>) -> Self {
let size = usize::try_from(region.size).expect("a region fits in memory");
let contents = if size == 0 {
NonNull::dangling()
} else {
let contents = NonNull::new(unsafe { std::alloc::alloc_zeroed(layout(size)) })
.unwrap_or_else(|| std::alloc::handle_alloc_error(layout(size)));
if region.erased != 0 {
unsafe { contents.as_ptr().write_bytes(region.erased, size) }
}
contents
};
Self {
base: region.base,
aliases,
writable: region.kind.is_writable(),
programmable: region.programmer.is_some(),
erased: region.erased,
contents,
size,
}
}
fn offset(&self, address: u64, length: u64) -> Option<usize> {
let size = self.size as u64;
self.starts()
.find(|&base| {
address >= base
&& address
.checked_add(length)
.zip(base.checked_add(size))
.is_some_and(|(end, limit)| end <= limit)
})
.map(|base| usize::try_from(address - base).expect("inside a region in memory"))
}
fn kept(&self) -> bool {
self.writable || self.programmable
}
fn starts(&self) -> impl Iterator<Item = u64> {
std::iter::once(self.base).chain(self.aliases.iter().copied())
}
fn at(&self, offset: usize) -> *mut u8 {
self.contents.as_ptr().wrapping_add(offset)
}
fn copy_out(&self, offset: usize, into: &mut [u8]) {
assert!(offset + into.len() <= self.size, "inside the region");
unsafe { std::ptr::copy_nonoverlapping(self.at(offset), into.as_mut_ptr(), into.len()) }
}
fn copy_in(&mut self, offset: usize, bytes: &[u8]) {
assert!(offset + bytes.len() <= self.size, "inside the region");
unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), self.at(offset), bytes.len()) }
}
}
#[derive(Debug)]
pub(crate) struct Memory {
regions: Vec<Bytes>,
endian: Endian,
}
#[derive(Debug)]
pub(crate) struct Mapped(Memory);
impl Mapped {
pub(crate) fn new(memory: Memory) -> Self {
Self(memory)
}
pub(crate) fn place(&mut self, address: u64, bytes: &[u8]) -> bool {
self.0.place(address, bytes)
}
pub(crate) fn write(&mut self, address: u64, width: Width, value: u64) -> bool {
self.0.write(address, width, value)
}
}
impl std::ops::Deref for Mapped {
type Target = Memory;
fn deref(&self) -> &Memory {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Mapping {
pub(crate) start: u64,
pub(crate) length: u64,
pub(crate) writable: bool,
}
pub(crate) fn to_target(value: u64, count: usize, endian: Endian) -> [u8; 8] {
let mut bytes = [0; 8];
match endian {
Endian::Little => bytes[..count].copy_from_slice(&value.to_le_bytes()[..count]),
Endian::Big => bytes[..count].copy_from_slice(&value.to_be_bytes()[8 - count..]),
}
bytes
}
pub(crate) fn from_target(bytes: &[u8], endian: Endian) -> u64 {
let mut word = [0; 8];
match endian {
Endian::Little => {
word[..bytes.len()].copy_from_slice(bytes);
u64::from_le_bytes(word)
}
Endian::Big => {
word[8 - bytes.len()..].copy_from_slice(bytes);
u64::from_be_bytes(word)
}
}
}
impl Memory {
pub(crate) fn new(map: &[Region], aliases: &[Alias], endian: Endian) -> Self {
Self {
regions: map
.iter()
.map(|region| {
Bytes::erased(
region,
aliases
.iter()
.filter(|alias| alias.region == region.name)
.map(|alias| alias.at)
.collect(),
)
})
.collect(),
endian,
}
}
pub(crate) fn endian(&self) -> Endian {
self.endian
}
#[cfg(not(emulator_absent))]
pub(crate) unsafe fn map_into(&self, processor: &mut dyn super::Processor) {
for region in &self.regions {
for start in region.starts() {
unsafe { processor.map(start, region.size as u64, region.at(0), region.writable) };
}
}
}
pub(crate) fn mappings(&self) -> Vec<Mapping> {
self.regions
.iter()
.flat_map(|region| {
region.starts().map(|start| Mapping {
start,
length: region.size as u64,
writable: region.writable,
})
})
.collect()
}
pub(crate) fn partial_page(&self, page: u64) -> Option<Mapping> {
self.mappings().into_iter().find(|mapping| {
mapping.length == 0
|| !mapping.start.is_multiple_of(page)
|| !mapping.length.is_multiple_of(page)
})
}
#[cfg(test)]
fn host(&self, address: u64) -> Option<*mut u8> {
self.regions.iter().find_map(|region| {
let offset = region.offset(address, 1)?;
Some(region.at(offset))
})
}
pub(crate) fn contents(&self, read_only: bool) -> Vec<(u64, Vec<u8>)> {
self.regions
.iter()
.filter(|region| read_only || region.kept())
.map(|region| {
let mut bytes = vec![0; region.size];
region.copy_out(0, &mut bytes);
(region.base, bytes)
})
.collect()
}
pub(crate) fn erased_unkept(&self) -> Vec<(u64, Vec<u8>)> {
self.regions
.iter()
.filter(|region| !region.kept())
.map(|region| (region.base, vec![region.erased; region.size]))
.collect()
}
pub(crate) fn read_only(&self, address: u64) -> bool {
self.holds(address, 1, |region| !region.writable)
}
fn holds(&self, address: u64, length: u64, keep: impl Fn(&Bytes) -> bool) -> bool {
self.regions
.iter()
.any(|region| keep(region) && region.offset(address, length).is_some())
}
pub(crate) fn read(&self, address: u64, width: Width) -> Option<u64> {
let bytes = usize::try_from(width.bytes()).expect("a width fits");
self.regions.iter().find_map(|region| {
let offset = region.offset(address, bytes as u64)?;
let mut word = [0; 8];
region.copy_out(offset, &mut word[..bytes]);
Some(from_target(&word[..bytes], self.endian))
})
}
pub(crate) fn write(&mut self, address: u64, width: Width, value: u64) -> bool {
let bytes = usize::try_from(width.bytes()).expect("a width fits");
self.place(address, &to_target(value, bytes, self.endian)[..bytes])
}
pub(crate) fn place(&mut self, address: u64, bytes: &[u8]) -> bool {
self.regions.iter_mut().any(|region| {
let Some(offset) = region.offset(address, bytes.len() as u64) else {
return false;
};
region.copy_in(offset, bytes);
true
})
}
pub(crate) fn programmable(&self, address: u64, width: Width) -> bool {
let end = address.checked_add(u64::from(width.bytes()));
self.regions.iter().any(|region| {
region.programmable
&& address >= region.base
&& end.is_some_and(|end| end <= region.base + region.size as u64)
})
}
pub(crate) fn kept(&self, address: u64) -> bool {
self.holds(address, 1, Bytes::kept)
}
pub(crate) fn writable(&self, address: u64) -> bool {
self.holds(address, 1, |region| region.writable)
}
pub(crate) fn changed(&self, address: u64, bytes: &[u8]) -> Option<Vec<Range<usize>>> {
const CHUNK: usize = 64;
let (region, offset) = self
.regions
.iter()
.find_map(|region| Some((region, region.offset(address, bytes.len() as u64)?)))?;
let mut held = vec![0; bytes.len()];
region.copy_out(offset, &mut held);
let mut spans: Vec<Range<usize>> = Vec::new();
for (index, (now, then)) in held.chunks(CHUNK).zip(bytes.chunks(CHUNK)).enumerate() {
if now == then {
continue;
}
let start = index * CHUNK;
match spans.last_mut() {
Some(last) if last.end == start => last.end = start + then.len(),
_ => spans.push(start..start + then.len()),
}
}
Some(spans)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn memory() -> Memory {
Memory::new(
&[
Region::flash("flash", 0x0800_0000, 0x1000).erased_to(0xFF),
Region::ram("sram", 0x2000_0000, 0x100),
],
&[Alias {
region: "flash",
at: 0,
}],
Endian::Little,
)
}
#[test]
fn contents_copies_out_each_region_whole() {
let mut memory = memory();
assert!(memory.place(0x0800_0004, &[1, 2]));
assert!(memory.place(0x2000_00FE, &[3, 4]));
let writable = memory.contents(false);
assert_eq!(writable.len(), 1);
let (base, ram) = &writable[0];
assert_eq!((*base, ram.len()), (0x2000_0000, 0x100));
assert_eq!(&ram[0xFE..], [3, 4]);
let every = memory.contents(true);
assert_eq!(
every
.iter()
.map(|(base, bytes)| (*base, bytes.len()))
.collect::<Vec<_>>(),
[(0x0800_0000, 0x1000), (0x2000_0000, 0x100)]
);
assert_eq!(&every[0].1[4..6], [1, 2]);
}
#[test]
fn a_programmed_region_is_kept_and_programmable_where_another_read_only_one_is_not() {
let memory = Memory::new(
&[
Region::flash("flash", 0x0800_0000, 0x1000).programmed_by("PROG"),
Region::system("system", 0x0900_0000, 0x100),
Region::ram("sram", 0x2000_0000, 0x100),
],
&[Alias {
region: "flash",
at: 0,
}],
Endian::Little,
);
assert!(memory.programmable(0x0800_0FFC, Width::W32));
assert!(
!memory.programmable(0x0800_0FFE, Width::W32),
"runs past the end"
);
assert!(!memory.programmable(0x10, Width::W32), "through the alias");
assert!(memory.kept(0x10), "the same bytes");
assert!(!memory.programmable(0x0900_0000, Width::W32));
assert!(
!memory.programmable(0x2000_0000, Width::W32),
"the guest writes it itself"
);
let kept: Vec<u64> = memory
.contents(false)
.iter()
.map(|(base, _)| *base)
.collect();
assert_eq!(kept, [0x0800_0000, 0x2000_0000]);
assert!(memory.kept(0x0800_0000) && memory.kept(0x2000_0000));
assert!(!memory.kept(0x0900_0000));
let erased: Vec<u64> = memory
.erased_unkept()
.iter()
.map(|(base, _)| *base)
.collect();
assert_eq!(erased, [0x0900_0000], "a snapshot keeps the programmed one");
}
#[test]
fn changed_gives_the_chunks_that_differ() {
let memory = memory();
let mut bytes = vec![0xFF; 0x1000];
assert_eq!(memory.changed(0x0800_0000, &bytes), Some(Vec::new()));
bytes[0x41] = 0;
bytes[0x80] = 0;
bytes[0xFFF] = 0;
assert_eq!(
memory.changed(0x0800_0000, &bytes),
Some(vec![0x40..0xC0, 0xFC0..0x1000])
);
assert_eq!(memory.changed(0x0800_0FF0, &[0xFF; 0x20]), None);
}
#[test]
fn a_region_starts_erased_and_is_given_back_erased() {
let mut memory = memory();
assert_eq!(memory.read(0x0800_0FFC, Width::W32), Some(0xFFFF_FFFF));
assert_eq!(memory.read(0x2000_0000, Width::W32), Some(0));
assert!(memory.place(0x0800_0004, &[1, 2]));
assert_eq!(memory.erased_unkept(), [(0x0800_0000, vec![0xFF; 0x1000])]);
}
#[test]
fn an_alias_is_the_same_bytes() {
let mut memory = memory();
assert!(memory.write(0x0800_0010, Width::W32, 0xDEAD_BEEF));
assert_eq!(memory.read(0x10, Width::W32), Some(0xDEAD_BEEF));
assert_eq!(memory.host(0x10), memory.host(0x0800_0010));
}
#[test]
fn every_region_is_mapped_at_its_address_and_its_aliases() {
let mappings = memory().mappings();
assert_eq!(
mappings,
[
Mapping {
start: 0x0800_0000,
length: 0x1000,
writable: false
},
Mapping {
start: 0,
length: 0x1000,
writable: false
},
Mapping {
start: 0x2000_0000,
length: 0x100,
writable: true
},
]
);
}
#[test]
fn a_region_that_ends_inside_a_page_is_found() {
assert_eq!(memory().partial_page(0x100), None);
assert_eq!(
memory().partial_page(0x400).map(|mapping| mapping.start),
Some(0x2000_0000)
);
}
#[test]
fn a_pointer_the_core_keeps_and_the_runtimes_own_access_share_the_bytes() {
let mut memory = memory();
let kept = memory.host(0x2000_0010).unwrap();
assert!(memory.place(0x2000_0010, &[1, 2]));
assert_eq!(unsafe { kept.read() }, 1);
unsafe { kept.write(9) };
assert_eq!(memory.read(0x2000_0010, Width::W8), Some(9));
assert!(memory.write(0x2000_0010, Width::W8, 7));
assert_eq!(unsafe { kept.read() }, 7);
}
#[test]
fn an_access_that_runs_off_a_region_is_nothing() {
let memory = memory();
assert_eq!(memory.read(0x2000_00FE, Width::W32), None);
assert_eq!(memory.read(0x3000_0000, Width::W8), None);
assert!(memory.read_only(0x10));
assert!(!memory.read_only(0x2000_0000));
assert!(memory.writable(0x2000_0000));
}
}