/** * @file Memory interface used to represent and access emulated memory and * MMIO devices. */ #pragma once #include #include #include #include #include #include #include struct AudioFrontend; class LogManager; class PicaContext; class InputSource; namespace EmuDisplay { struct EmuDisplay; } /** * Memory namespace */ namespace Memory { // TODO: Change this to a strongly typed enum to aid the compiler optimizer in pointer analysis using EmulatedMemory = uint8_t; using PAddr = uint32_t; struct HostMemoryBackedPage { // nullptr if no memory backed page exists EmulatedMemory* data = nullptr; explicit operator bool() const { return (data != nullptr); } }; // Set of contiguous pages in memory backed by host RAM struct HostMemoryBackedPages { EmulatedMemory* data = nullptr; uint32_t num_bytes = 0; explicit operator bool() const { return (data != nullptr); } }; /** * Type representing a the physical address range of a memory bus. */ template struct Bus { static constexpr uint32_t start = PAddrStart; static constexpr uint32_t size = Size; static constexpr uint32_t end = start + size; uint8_t Read8(uint32_t address); uint16_t Read16(uint32_t address); uint32_t Read32(uint32_t address); void Write8(uint32_t address, uint8_t value); void Write16(uint32_t address, uint16_t value); void Write32(uint32_t address, uint32_t value); static_assert(start < static_cast(start + size), "End address wrapped due to overflow... wrong template parameters?"); }; enum class HookKind { Read, Write, ReadWrite }; constexpr bool HasReadHook(HookKind kind) noexcept { return (kind == HookKind::Read || kind == HookKind::ReadWrite); } constexpr bool HasWriteHook(HookKind kind) noexcept { return (kind == HookKind::Write || kind == HookKind::ReadWrite); } constexpr HookKind AddReadHook(HookKind kind) noexcept { return (kind == HookKind::Write) ? HookKind::ReadWrite : HookKind::Read; } constexpr HookKind AddWriteHook(HookKind kind) noexcept { return (kind == HookKind::Read) ? HookKind::ReadWrite : HookKind::Write; } template struct Hook; using PAddr = uint32_t; struct MemoryRange { PAddr start; uint32_t num_bytes; }; struct HookBase { // Actual memory range this hook is active for MemoryRange range; // Other hook contained in this same page // Guaranteed to be non-overlapping and to cover only memory *after* this hook std::unique_ptr next {}; }; template struct HookHandler; template<> struct HookHandler { virtual void OnRead(PAddr addr, uint32_t num_bytes) = 0; }; template<> struct HookHandler { virtual void OnWrite(PAddr addr, uint32_t num_bytes, uint32_t value) = 0; }; using ReadHandler = HookHandler; using WriteHandler = HookHandler; // Arguments: Address and value size template<> struct Hook : HookBase { Hook(MemoryRange range, ReadHandler& handler_) : HookBase { range }, handler(handler_) {} ReadHandler& handler; }; // Arguments: Address, value, and value size template<> struct Hook : HookBase { Hook(MemoryRange range, WriteHandler& handler_) : HookBase { range }, handler(handler_) {} WriteHandler& handler; }; using ReadHook = Hook; using WriteHook = Hook; // TODO: Find optimal alignment (check VEC_SIZE in glibc? Might just be 64 for AVX2). Having any alignment at all helps the compiler generate better memset()s for initialization template struct alignas(512) MemoryBus : Bus { // NOTE: If we over-allocate this array (by adding 3 more entries than strictly necessary), we can avoid needing range checks! (or at least we can reshuffle them past the actual writes to benefit from speculative execution) uint8_t data[Size]; std::array, (Size >> 12)> write_hooks; std::array, (Size >> 12)> read_hooks; // TODO: When no hooks are set, callers should always use direct memory // access instead of calling the Read/Write* functions. Currently, // ReadLegacy/WriteLegacy is used in many places still, but once // that is deprecated we should assert that a hook is set when // using Write*. uint8_t Read8(uint32_t address) { assert(address < this->end); if (auto* hook = LookupReadHookFor(address)) { hook->handler.OnRead(address, sizeof(uint8_t)); } else { // TODO: See above } return data[address - PAddrStart]; } uint16_t Read16(uint32_t address) { if (auto* hook = LookupReadHookFor(address)) { hook->handler.OnRead(address, sizeof(uint16_t)); } else { // TODO: See above } // TODO: Endianness uint16_t ret; memcpy(&ret, &data[address - PAddrStart], sizeof(ret)); return ret; } uint32_t Read32(uint32_t address) { if (auto* hook = LookupReadHookFor(address)) { hook->handler.OnRead(address, sizeof(uint32_t)); } else { // TODO: See above } // TODO: Endianness uint32_t ret; memcpy(&ret, &data[address - PAddrStart], sizeof(ret)); return ret; } void Write8(uint32_t address, uint8_t value) { assert(address < this->end); if (auto* hook = LookupWriteHookFor(address)) { hook->handler.OnWrite(address, sizeof(value), value); } else { // TODO: See above } data[address - PAddrStart] = value; } void Write16(uint32_t address, uint16_t value) { if (auto* hook = LookupWriteHookFor(address)) { hook->handler.OnWrite(address, sizeof(value), value); } else { // TODO: See above } // TODO: Endianness memcpy(&data[address - PAddrStart], &value, sizeof(value)); } void Write32(uint32_t address, uint32_t value) { if (auto* hook = LookupWriteHookFor(address)) { hook->handler.OnWrite(address, sizeof(value), value); } else { // TODO: See above } // TODO: Endianness memcpy(&data[address - PAddrStart], &value, sizeof(value)); } private: friend HostMemoryBackedPages LookupContiguousMemoryBackedPage(struct PhysicalMemory&, PAddr, uint32_t); template friend HostMemoryBackedPages LookupContiguousMemoryBackedPage(struct PhysicalMemory&, PAddr, uint32_t); WriteHook* LookupWriteHookFor(uint32_t address) noexcept { return static_cast(write_hooks[(address - PAddrStart) >> 12].get()); } ReadHook* LookupReadHookFor(uint32_t address) noexcept { return static_cast(read_hooks[(address - PAddrStart) >> 12].get()); } }; struct MemoryAccessHandler { uint8_t Read8(uint32_t offset); uint16_t Read16(uint32_t offset); uint32_t Read32(uint32_t offset); void Write8(uint32_t offset, uint8_t value); void Write16(uint32_t offset, uint16_t value); void Write32(uint32_t offset, uint32_t value); }; /** * Proxy bus descriptor structure that forwards all reads and writes to a * handler given by T. This allows us to define MMIO handlers in external * translation units and forward-declaring them in this file instead of * defining them all in this header. * @tparam T handler structure to forward Read/Write function calls to. Must be a base of MemoryAccessHandler */ template struct ProxyBus : MemoryBus { std::unique_ptr handler; // Constructors and destructors are defined outside the header since T is // not a complete type in most translation units (which is necessary to // implement this destructor). ProxyBus(std::unique_ptr handler); ProxyBus(ProxyBus&& bus); ~ProxyBus(); uint8_t Read8(uint32_t address); uint16_t Read16(uint32_t address); uint32_t Read32(uint32_t address); void Write8(uint32_t address, uint8_t value); void Write16(uint32_t address, uint16_t value); void Write32(uint32_t address, uint32_t value); }; struct MPCorePrivate; struct HID; struct LCD; struct AXIHandler; struct GPU; struct HASH; struct GPIO; struct SPI; struct CONFIG11; struct DSPMMIO; struct DSPMemory; using VRAM = MemoryBus<0x18000000, 0x00600000>; //using DSP = MemoryBus<0x1ff00000, 0x00080000>; using DSP = ProxyBus; using AXI = MemoryBus<0x1ff80000, 0x00080000>; //using FCRAM = MemoryBus<0x20000000, 0x08000000>; // Upper 0x08000000 bytes only available on New3DS! // using FCRAM = MemoryBus<0x20000000, 0x10000000>; using FCRAM = MemoryBus<0x20000000, 0x0800'0000>; using IO_HASH = ProxyBus; using IO_DSP1 = ProxyBus; // TODO: Actually CSND registers using IO_CONFIG11 = ProxyBus; using IO_SPIBUS2 = ProxyBus; // SPI devices 3/4/5 using IO_SPIBUS3 = ProxyBus; // SPI device 6 using IO_HID = ProxyBus; using IO_GPIO = ProxyBus; using IO_SPIBUS1 = ProxyBus; // SPI devices 0/1/2 (3/4/5 at 0x10142000, 6 at 0x10143000) using IO_LCD = ProxyBus; using IO_DSP2 = ProxyBus; // TODO: Is this indeed just a mirror of 0x10103000? TODO: Actually the real DSP registers using IO_AXI = ProxyBus; using IO_HASH2 = ProxyBus; // NOTE: This is *NOT* just a mirror of 0x10101000 ... (3dbrew erratum) using IO_GPU = ProxyBus; using MPCorePrivateBus = ProxyBus; struct PhysicalMemorySubscriber; struct PhysicalMemory { using Busses = std::tuple; Busses memory; std::vector subscribers; /** * @note This constructor leaves the memory system in a partially * unintialized state. To finalize initialization, some of the busses * need to be connected to their respective end points. */ PhysicalMemory(LogManager& log_manager); void InjectDependency(AudioFrontend&); void InjectDependency(PicaContext&); void InjectDependency(InputSource&); }; struct PhysicalMemorySubscriber { PhysicalMemorySubscriber(PhysicalMemory& mem_) : mem(mem_) { mem.subscribers.push_back(this); } PhysicalMemory& mem; virtual ~PhysicalMemorySubscriber() { for (auto it = mem.subscribers.begin(); it != mem.subscribers.end(); ++it) { if (*it == this) { mem.subscribers.erase(it); break; } } } // Called when a MemoryBus in PhysicalMemory transitions from // "not backed by host memory" to "backed by host memory" state virtual void OnBackedByHostMemory(HostMemoryBackedPage page, uint32_t address) { // Do nothing by default } // Called when a MemoryBus in PhysicalMemory transitions from // "backed by host memory" to "not backed by host memory" state virtual void OnUnbackedByHostMemory(uint32_t address) { // Do nothing by default } }; struct IsInside { uint32_t address; template bool operator()(const Bus& bus) const { return address >= bus.start && address < bus.end; } }; namespace detail { /// Utility functor to call the appropriate Read8/Read16/Read32 variant based on the given data type template struct BusReader { uint32_t address; template DataType operator()(Bus& bus) const { static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "Invalid DataType"); if constexpr (std::is_same::value) { return bus.Read8(address); } else if constexpr (std::is_same::value) { return bus.Read16(address); } else if constexpr (std::is_same::value) { return bus.Read32(address); } } }; /// Utility functor to call the appropriate Read8/Read16/Read32 variant based on the given data type template struct BusWriter { uint32_t address; template void operator()(Bus& bus, DataType value) const { static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "Invalid DataType"); if (std::is_same::value) { return bus.Write8(address, value); } else if (std::is_same::value) { return bus.Write16(address, value); } else if (std::is_same::value) { return bus.Write32(address, value); } } }; template DataType ReadHelper(PhysicalMemory& mem, uint32_t address, std::index_sequence) { DataType data; auto attempt_read = [&](auto&& bus) { if (IsInside{address}(bus)) { data = BusReader{address}(bus); return true; } else { return false; } }; // Using short-circuit evaluation here to keep testing until we find the right bus (but no further than that) bool match_found = (attempt_read(std::get(mem.memory)) || ...); if (!match_found) throw std::runtime_error(fmt::format("Read from unknown physical address {:#010x}", address)); return data; } template void WriteHelper(PhysicalMemory& mem, uint32_t address, DataType value, std::index_sequence) { auto attempt_write = [&](auto&& bus) { if (IsInside{address}(bus)) { BusWriter{address}(bus, value); return true; } else { return false; } }; // Using short-circuit evaluation here to keep testing until we find the right bus (but no further than that) bool match_found = (attempt_write(std::get(mem.memory)) || ...); if (!match_found) throw std::runtime_error(fmt::format("Write to unknown physical address {:#010x}", address)); } template bool ForEachMemoryBusHelper(Bus& bus, Callback& callback) { if constexpr (std::is_base_of_v, Bus>) { return callback(bus); } return false; } // Iterates over all MemoryBusses, calling callback on each of them until it returns true template bool ForEachMemoryBus(std::tuple& busses, Callback& callback) { return (ForEachMemoryBusHelper(std::get(busses), callback) || ...); } template HostMemoryBackedPage GetMemoryBackedPageFor(Bus& bus, PAddr address) { return { bus.data + (address - bus.start) }; } } // namespace detail /** * Read from PhysicalMemory under the constraint that only the address ranges on * the BusTuple are considered. * @throws std::runtime_error when the given address is outside any of the * given Bus address ranges */ template DataType ReadLegacy(PhysicalMemory& mem, uint32_t address) { // TODO: Statically assert that BusTuple is a subtuple of mem.busses // TODO: Restrict search to the given BusTuple constexpr size_t length = std::tuple_size::value; return detail::ReadHelper(mem, address, std::make_index_sequence{}); } /** * Write to PhysicalMemory under the constraint that only the address ranges on * the BusTuple are considered. * @throws std::runtime_error when the given address is outside any of the * given Bus address ranges */ template void WriteLegacy(PhysicalMemory& mem, uint32_t address, DataType value) { // TODO: Statically assert that BusTuple is a subtuple of mem.busses // TODO: Restrict search to the given BusTuple constexpr size_t length = std::tuple_size::value; detail::WriteHelper(mem, address, value, std::make_index_sequence{}); } template inline constexpr bool IsMemoryBus(const MemoryBus&) { return true; } template inline constexpr bool IsMemoryBus(const T&) { return false; } inline HostMemoryBackedPage LookupMemoryBackedPage(PhysicalMemory& mem, PAddr address) { HostMemoryBackedPage out_page = { nullptr }; auto callback = [&](auto& bus) { if (IsInside{address}(bus)) { // Return null page if this is not a memory bus // TODO: Also return null page if any hooks are set up // if (!bus.write_hooks[(address - bus.start) >> 12] && !bus.read_hooks[(address - bus.start) >> 12]) { if (IsMemoryBus(bus)) { out_page = detail::GetMemoryBackedPageFor(bus, address); } return true; } return false; }; detail::ForEachMemoryBus(mem.memory, callback); return out_page; } // Deprecated since this doesn't check for or trigger any memory hooks [[deprecated]] HostMemoryBackedPages LookupContiguousMemoryBackedPage(PhysicalMemory& mem, PAddr address, uint32_t num_bytes); template HostMemoryBackedPages LookupContiguousMemoryBackedPage(PhysicalMemory& mem, PAddr address, uint32_t num_bytes); template DataType Read(HostMemoryBackedPage page, uint32_t offset) noexcept { static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "Invalid DataType"); if constexpr (std::is_same_v) { return page.data[offset]; } else { DataType ret; memcpy(&ret, &page.data[offset], sizeof(ret)); return boost::endian::little_to_native(ret); } } template DataType Read(HostMemoryBackedPages pages, uint32_t offset) noexcept { return Read(HostMemoryBackedPage { pages.data }, offset); } template void Write(HostMemoryBackedPage page, uint32_t offset, DataType value) noexcept { static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "Invalid DataType"); if constexpr (std::is_same_v) { page.data[offset] = value; } else { value = boost::endian::native_to_little(value); memcpy(&page.data[offset], &value, sizeof(value)); } } template void Write(HostMemoryBackedPages pages, uint32_t offset, DataType value) noexcept { Write(HostMemoryBackedPage { pages.data }, offset, value); } /** * Assigns the given Hook to all pages in the specified address range. * * @pre The address range may not cross a bus boundary */ template void SetHook(PhysicalMemory&, PAddr start, uint32_t num_bytes, HookHandler&); template auto GenerateHooks(PhysicalMemory& mem, PAddr start, uint32_t num_bytes, const F& callback) -> std::enable_if_t*, F, PAddr>> { static_assert( Kind == HookKind::Read || Kind == HookKind::Write, "Must pick either read or write hooks"); uint32_t end_page_index = ((start & 0xfff) + 0x1000 + num_bytes - 1) >> 12; for (uint32_t page_index = 0; page_index < end_page_index; ++page_index) { PAddr page_start = ((start >> 12) + page_index) << 12; // Pass 1 as the region size for SetWriteHook since it's not used for single-page assignments anyway PAddr hook_range_start = page_index == 0 ? start : page_start; PAddr hook_range_end = (page_index == end_page_index - 1) ? start + num_bytes : (page_start + 0x1000); auto hook = callback(page_start); if (hook) { SetHook(mem, hook_range_start, (hook_range_end - hook_range_start), *hook); } } } // TODO: Should read the expected HookHandler& for validation void ClearHook(PhysicalMemory& mem, HookKind kind, PAddr start, uint32_t num_bytes); } // namespace Memory