use std::{ collections::HashMap, fmt::Debug, iter::FusedIterator, sync::{atomic::AtomicU64, Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak}, }; use bytemuck::BoxBytes; use itertools::Itertools; use tracing::warn; use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; pub struct MemoryClient { client: EmulatorClient, regions: Mutex>>, } impl MemoryClient { pub fn new(client: EmulatorClient) -> Self { Self { client, regions: Mutex::new(HashMap::new()), } } pub fn watch(&self, sim: SimId, start: u32, length: usize) -> MemoryView { let range = MemoryRange { sim, start, length }; let mut regions = self.regions.lock().unwrap_or_else(|e| e.into_inner()); let region = regions .get(&range) .and_then(|r| r.upgrade()) .unwrap_or_else(|| { let region = Arc::new(MemoryRegion::new(start, length)); regions.insert(range, Arc::downgrade(®ion)); self.client .send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(®ion))); region }); MemoryView { region } } pub fn write(&self, sim: SimId, address: u32, data: &T) { let mut buffer = vec![]; data.to_bytes(&mut buffer); let (tx, _) = oneshot::channel(); self.client .send_command(EmulatorCommand::WriteMemory(sim, address, buffer, tx)); } } fn aligned_memory(start: u32, length: usize) -> BoxBytes { if start % 4 == 0 && length % 4 == 0 { let memory = vec![0u32; length / 4].into_boxed_slice(); return bytemuck::box_bytes_of(memory); } if start % 2 == 0 && length % 2 == 0 { let memory = vec![0u16; length / 2].into_boxed_slice(); return bytemuck::box_bytes_of(memory); } let memory = vec![0u8; length].into_boxed_slice(); bytemuck::box_bytes_of(memory) } pub struct MemoryView { region: Arc, } impl MemoryView { pub fn borrow(&self) -> MemoryRef<'_> { self.region.borrow() } } pub struct MemoryRef<'a> { inner: RwLockReadGuard<'a, BoxBytes>, } pub trait MemoryValue { fn from_bytes(bytes: &[u8]) -> Self; fn to_bytes(&self, buffer: &mut Vec); } macro_rules! primitive_memory_value_impl { ($T:ty, $L: expr) => { impl MemoryValue for $T { #[inline] fn from_bytes(bytes: &[u8]) -> Self { let bytes: [u8; std::mem::size_of::<$T>()] = std::array::from_fn(|i| bytes[i]); <$T>::from_le_bytes(bytes) } #[inline] fn to_bytes(&self, buffer: &mut Vec) { buffer.extend_from_slice(&self.to_le_bytes()) } } }; } primitive_memory_value_impl!(u8, 1); primitive_memory_value_impl!(u16, 2); primitive_memory_value_impl!(u32, 4); impl MemoryValue for [T; N] { #[inline] fn from_bytes(bytes: &[u8]) -> Self { std::array::from_fn(|i| { T::from_bytes(&bytes[i * std::mem::size_of::()..(i + 1) * std::mem::size_of::()]) }) } #[inline] fn to_bytes(&self, buffer: &mut Vec) { for item in self { item.to_bytes(buffer); } } } pub struct MemoryIter<'a, T> { bytes: &'a [u8], _phantom: std::marker::PhantomData, } impl<'a, T> MemoryIter<'a, T> { fn new(bytes: &'a [u8]) -> Self { Self { bytes, _phantom: std::marker::PhantomData, } } } impl Iterator for MemoryIter<'_, T> { type Item = T; #[inline] fn next(&mut self) -> Option { let (bytes, rest) = self.bytes.split_at_checked(std::mem::size_of::())?; self.bytes = rest; Some(T::from_bytes(bytes)) } #[inline] fn size_hint(&self) -> (usize, Option) { let size = self.bytes.len() / std::mem::size_of::(); (size, Some(size)) } } impl DoubleEndedIterator for MemoryIter<'_, T> { fn next_back(&mut self) -> Option { let mid = self.bytes.len().checked_sub(std::mem::size_of::())?; // SAFETY: the checked_sub above is effectively a bounds check let (rest, bytes) = unsafe { self.bytes.split_at_unchecked(mid) }; self.bytes = rest; Some(T::from_bytes(bytes)) } } impl FusedIterator for MemoryIter<'_, T> {} impl MemoryRef<'_> { pub fn read(&self, index: usize) -> T { let from = index * size_of::(); let to = from + size_of::(); T::from_bytes(&self.inner[from..to]) } pub fn range(&self, start: usize, count: usize) -> MemoryIter { let from = start * size_of::(); let to = from + (count * size_of::()); MemoryIter::new(&self.inner[from..to]) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct MemoryRange { pub sim: SimId, pub start: u32, pub length: usize, } const BUFFERS: usize = 4; pub struct MemoryRegion { gens: [AtomicU64; BUFFERS], bufs: [RwLock; BUFFERS], } impl Debug for MemoryRegion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MemoryRegion") .field("gens", &self.gens) .finish_non_exhaustive() } } // SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version unsafe impl Send for MemoryRegion {} // SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version unsafe impl Sync for MemoryRegion {} impl MemoryRegion { fn new(start: u32, length: usize) -> Self { Self { gens: std::array::from_fn(|i| AtomicU64::new(i as u64)), bufs: std::array::from_fn(|_| RwLock::new(aligned_memory(start, length))), } } pub fn borrow(&self) -> MemoryRef<'_> { /* * When reading memory, a thread will grab the newest buffer (with the highest gen) * It will only fail to grab the lock if the writer already has it, * but the writer prioritizes older buffers (with lower gens). * So this method will only block if the writer produces three full buffers * in the time it takes the reader to do four atomic reads and grab a lock. * In the unlikely event this happens... just try again. */ loop { let newest_index = self .gens .iter() .map(|i| i.load(std::sync::atomic::Ordering::Acquire)) .enumerate() .max_by_key(|(_, gen)| *gen) .map(|(i, _)| i) .unwrap(); let inner = match self.bufs[newest_index].try_read() { Ok(inner) => inner, Err(TryLockError::Poisoned(e)) => e.into_inner(), Err(TryLockError::WouldBlock) => { continue; } }; break MemoryRef { inner }; } } pub fn update(&self, data: &[u8]) { let gens: Vec = self .gens .iter() .map(|i| i.load(std::sync::atomic::Ordering::Acquire)) .collect(); let next_gen = gens.iter().max().unwrap() + 1; let indices = gens .into_iter() .enumerate() .sorted_by_key(|(_, val)| *val) .map(|(i, _)| i); for index in indices { let mut lock = match self.bufs[index].try_write() { Ok(inner) => inner, Err(TryLockError::Poisoned(e)) => e.into_inner(), Err(TryLockError::WouldBlock) => { continue; } }; lock.copy_from_slice(data); self.gens[index].store(next_gen, std::sync::atomic::Ordering::Release); return; } /* * We have four buffers, and (at time of writing) only three threads interacting with memory: * - The UI thread, reading small regions of memory * - The "vram renderer" thread, reading large regions of memory * - The emulation thread, writing memory every so often * So it should be impossible for all four buffers to have a read lock at the same time, * and (because readers always read the newest buffer) at least one of the oldest three * buffers will be free the entire time we're in this method. * TL;DR this should never happen. * But if it does, do nothing. This isn't medical software, better to show stale data than crash. */ warn!("all buffers were locked by a reader at the same time") } }