#pragma once #include #include #include #include #include #include #include #include namespace psprecomp { // Each generated AOT unit is one enormous function, and MSVC stops inlining // into a caller that large -- measured: moving the fast paths into this header // grew the linked image by only 2 KB, so the calls survived. Roughly a third of // the translated instructions are guest loads and stores, so the call has to go. #if defined(_MSC_VER) #define PSPRECOMP_MEMORY_FAST_PATH __forceinline #else #define PSPRECOMP_MEMORY_FAST_PATH inline __attribute__((always_inline)) #endif class GuestMemory { public: static constexpr std::uint32_t kVramPhysicalBase = 0x04000000u; static constexpr std::uint32_t kVramSize = 2u * 1024u * 1024u; static constexpr std::uint32_t kVramMirrorCount = 4u; static constexpr std::uint32_t kVramAddressSpan = kVramSize * kVramMirrorCount; static constexpr std::uint32_t kPhysicalBase = 0x08000000u; explicit GuestMemory(std::uint32_t size_bytes = 32u * 1024u * 1024u); ~GuestMemory(); // The AOT fast paths below index cached region pointers, so an instance may // not be relocated after construction. Runtime owns exactly one by value // and never copies it; making that explicit turns a future copy into a // compile error instead of a dangling read. GuestMemory(const GuestMemory &) = delete; GuestMemory &operator=(const GuestMemory &) = delete; GuestMemory(GuestMemory &&) = delete; GuestMemory &operator=(GuestMemory &&) = delete; [[nodiscard]] std::uint32_t size() const noexcept; [[nodiscard]] std::uint32_t vram_size() const noexcept; // Allegrex uses cached/uncached MIPS aliases; this maps addresses such as // 0x44000000 and 0x88000000 to the physical VRAM/RAM windows. // // Inline for the same reason as the AOT fast paths below: it is one AND, and // Runtime::invoke_chained_call performs it on every translated guest call // from 234 separate translation units, where an out-of-line accessor is a // real call into psprecomp_core. [[nodiscard]] static constexpr std::uint32_t canonical(std::uint32_t address) noexcept { return address & 0x1FFFFFFFu; } [[nodiscard]] bool contains(std::uint32_t address, std::size_t length = 1u) const noexcept; // cached AOT memory view. Generated units contain hundreds to // thousands of guest loads/stores each. Calling the inline GuestMemory // accessors still asks the optimizer to rediscover ram_data_, three limits // and the immutable write-watch flag at every static site. Materialize those // values once when a unit is entered, then keep them as ordinary locals that // MSVC/LTCG can retain in registers across the unit's basic blocks. // // The underlying RAM/VRAM vectors never resize after construction and // write_watch_enabled_ is fixed by the constructor, so this view remains // valid across nested AOT/HLE calls. Slow/VRAM paths delegate to the owning // GuestMemory and preserve the original validation/watch behavior. class AotFastView { public: // V8.4 production AOT path. VCSNative now treats direct-fastmem as a // launch requirement, so its generated corpus can call these helpers // and remove the per-memory-access `fastmem_base_ != nullptr` branch. // The checked AOT helpers below remain untouched for the generic // PSPRecomp framework, tests, tools and profiles that need fallback // memory semantics. [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint8_t aot_direct_load8(std::uint32_t address) const noexcept { return *fastmem_pointer(fastmem_base_, address); } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint16_t aot_direct_load16(std::uint32_t address) const noexcept { return GuestMemory::read_le16(fastmem_pointer(fastmem_base_, address)); } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint32_t aot_direct_load32(std::uint32_t address) const noexcept { return GuestMemory::read_le32(fastmem_pointer(fastmem_base_, address)); } PSPRECOMP_MEMORY_FAST_PATH void aot_direct_store8(std::uint32_t address, std::uint8_t value) const noexcept { *fastmem_pointer(fastmem_base_, address) = value; } PSPRECOMP_MEMORY_FAST_PATH void aot_direct_store16(std::uint32_t address, std::uint16_t value) const noexcept { GuestMemory::write_le16(fastmem_pointer(fastmem_base_, address), value); } PSPRECOMP_MEMORY_FAST_PATH void aot_direct_store32(std::uint32_t address, std::uint32_t value) const noexcept { GuestMemory::write_le32(fastmem_pointer(fastmem_base_, address), value); } template PSPRECOMP_MEMORY_FAST_PATH void aot_direct_load32_block( std::uint32_t address, std::uint32_t (&values)[N]) const noexcept { static_assert(N != 0u); if constexpr (std::endian::native == std::endian::little) { std::memcpy(values, fastmem_pointer(fastmem_base_, address), N * sizeof(std::uint32_t)); } else { for (std::size_t i = 0; i < N; ++i) values[i] = GuestMemory::read_le32(fastmem_pointer( fastmem_base_, address + static_cast(i * 4u))); } } template PSPRECOMP_MEMORY_FAST_PATH void aot_direct_store32_block( std::uint32_t address, const std::uint32_t (&values)[N]) const noexcept { static_assert(N != 0u); if constexpr (std::endian::native == std::endian::little) { std::memcpy(fastmem_pointer(fastmem_base_, address), values, N * sizeof(std::uint32_t)); } else { for (std::size_t i = 0; i < N; ++i) GuestMemory::write_le32(fastmem_pointer( fastmem_base_, address + static_cast(i * 4u)), values[i]); } } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint32_t aot_direct_load_word_left( std::uint32_t address, std::uint32_t existing) const noexcept { const std::uint32_t shift = (address & 3u) * 8u; const std::uint32_t memory_word = aot_direct_load32(address & ~3u); return (existing & (0x00FFFFFFu >> shift)) | (memory_word << (24u - shift)); } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint32_t aot_direct_load_word_right( std::uint32_t address, std::uint32_t existing) const noexcept { const std::uint32_t shift = (address & 3u) * 8u; const std::uint32_t memory_word = aot_direct_load32(address & ~3u); return (existing & (0xFFFFFF00u << (24u - shift))) | (memory_word >> shift); } PSPRECOMP_MEMORY_FAST_PATH void aot_direct_store_word_left( std::uint32_t address, std::uint32_t value) const noexcept { const std::uint32_t shift = (address & 3u) * 8u; const std::uint32_t aligned = address & ~3u; const std::uint32_t memory_word = aot_direct_load32(aligned); aot_direct_store32(aligned, (value >> (24u - shift)) | (memory_word & (0xFFFFFF00u << shift))); } PSPRECOMP_MEMORY_FAST_PATH void aot_direct_store_word_right( std::uint32_t address, std::uint32_t value) const noexcept { const std::uint32_t shift = (address & 3u) * 8u; const std::uint32_t aligned = address & ~3u; const std::uint32_t memory_word = aot_direct_load32(aligned); aot_direct_store32(aligned, (value << shift) | (memory_word & (0x00FFFFFFu >> (24u - shift)))); } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint8_t aot_load8(std::uint32_t address) const { if (fastmem_base_ != nullptr) return *fastmem_pointer(fastmem_base_, address); const std::uint32_t offset = ram_offset_of_fast(address); if (offset <= ram_limit8_) return ram_data_[offset]; return owner_->aot_load8_slow(address); } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint16_t aot_load16(std::uint32_t address) const { if (fastmem_base_ != nullptr) return GuestMemory::read_le16(fastmem_pointer(fastmem_base_, address)); const std::uint32_t offset = ram_offset_of_fast(address); if (offset <= ram_limit16_) return GuestMemory::read_le16(ram_data_ + offset); return owner_->aot_load16_slow(address); } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint32_t aot_load32(std::uint32_t address) const { if (fastmem_base_ != nullptr) return GuestMemory::read_le32(fastmem_pointer(fastmem_base_, address)); const std::uint32_t offset = ram_offset_of_fast(address); if (offset <= ram_limit32_) return GuestMemory::read_le32(ram_data_ + offset); return owner_->aot_load32_slow(address); } PSPRECOMP_MEMORY_FAST_PATH void aot_store8(std::uint32_t address, std::uint8_t value) const { #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (fastmem_base_ != nullptr) { *fastmem_pointer(fastmem_base_, address) = value; return; } #else if (fastmem_base_ != nullptr && !write_watch_enabled_) { *fastmem_pointer(fastmem_base_, address) = value; return; } #endif const std::uint32_t offset = ram_offset_of_fast(address); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (offset <= ram_limit8_) { #else if (!write_watch_enabled_ && offset <= ram_limit8_) { #endif ram_data_[offset] = value; return; } owner_->aot_store8_slow(address, value); } PSPRECOMP_MEMORY_FAST_PATH void aot_store16(std::uint32_t address, std::uint16_t value) const { #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (fastmem_base_ != nullptr) { GuestMemory::write_le16(fastmem_pointer(fastmem_base_, address), value); return; } #else if (fastmem_base_ != nullptr && !write_watch_enabled_) { GuestMemory::write_le16(fastmem_pointer(fastmem_base_, address), value); return; } #endif const std::uint32_t offset = ram_offset_of_fast(address); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (offset <= ram_limit16_) { #else if (!write_watch_enabled_ && offset <= ram_limit16_) { #endif GuestMemory::write_le16(ram_data_ + offset, value); return; } owner_->aot_store16_slow(address, value); } PSPRECOMP_MEMORY_FAST_PATH void aot_store32(std::uint32_t address, std::uint32_t value) const { #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (fastmem_base_ != nullptr) { GuestMemory::write_le32(fastmem_pointer(fastmem_base_, address), value); return; } #else if (fastmem_base_ != nullptr && !write_watch_enabled_) { GuestMemory::write_le32(fastmem_pointer(fastmem_base_, address), value); return; } #endif const std::uint32_t offset = ram_offset_of_fast(address); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (offset <= ram_limit32_) { #else if (!write_watch_enabled_ && offset <= ram_limit32_) { #endif GuestMemory::write_le32(ram_data_ + offset, value); return; } owner_->aot_store32_slow(address, value); } // Tier-2 dataflow helpers: collapse a run of adjacent 32-bit guest // accesses into one address canonicalization / bounds check. These are // deliberately available only through AotFastView so generated Tier-2 // code can use them without changing the generic GuestMemory contract. // // On the little-endian hosts VCS targets, memcpy lets MSVC/GCC emit a // compact vector move for small compile-time N. Slow/EDRAM/watch paths // retain the exact per-word behavior and ordering of the original AOT. template [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH bool aot_try_load32_block( std::uint32_t address, std::uint32_t (&values)[N]) const { static_assert(N != 0u); constexpr std::uint32_t kTail = static_cast((N - 1u) * 4u); if (fastmem_base_ != nullptr) { if constexpr (std::endian::native == std::endian::little) { std::memcpy(values, fastmem_pointer(fastmem_base_, address), N * sizeof(std::uint32_t)); } else { for (std::size_t i = 0; i < N; ++i) values[i] = GuestMemory::read_le32(fastmem_pointer(fastmem_base_, address + static_cast(i * 4u))); } return true; } const std::uint32_t offset = ram_offset_of_fast(address); if (offset > ram_limit32_ || kTail > (ram_limit32_ - offset)) return false; if constexpr (std::endian::native == std::endian::little) { std::memcpy(values, ram_data_ + offset, N * sizeof(std::uint32_t)); } else { for (std::size_t i = 0; i < N; ++i) values[i] = GuestMemory::read_le32(ram_data_ + offset + i * 4u); } return true; } template PSPRECOMP_MEMORY_FAST_PATH void aot_load32_block( std::uint32_t address, std::uint32_t (&values)[N]) const { if (aot_try_load32_block(address, values)) return; for (std::size_t i = 0; i < N; ++i) values[i] = aot_load32(address + static_cast(i * 4u)); } template [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH bool aot_try_store32_block( std::uint32_t address, const std::uint32_t (&values)[N]) const { static_assert(N != 0u); constexpr std::uint32_t kTail = static_cast((N - 1u) * 4u); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (fastmem_base_ != nullptr) { #else if (fastmem_base_ != nullptr && !write_watch_enabled_) { #endif if constexpr (std::endian::native == std::endian::little) { std::memcpy(fastmem_pointer(fastmem_base_, address), values, N * sizeof(std::uint32_t)); } else { for (std::size_t i = 0; i < N; ++i) GuestMemory::write_le32(fastmem_pointer(fastmem_base_, address + static_cast(i * 4u)), values[i]); } return true; } const std::uint32_t offset = ram_offset_of_fast(address); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) const bool direct = offset <= ram_limit32_ && kTail <= (ram_limit32_ - offset); #else const bool direct = !write_watch_enabled_ && offset <= ram_limit32_ && kTail <= (ram_limit32_ - offset); #endif if (!direct) return false; if constexpr (std::endian::native == std::endian::little) { std::memcpy(ram_data_ + offset, values, N * sizeof(std::uint32_t)); } else { for (std::size_t i = 0; i < N; ++i) GuestMemory::write_le32(ram_data_ + offset + i * 4u, values[i]); } return true; } template PSPRECOMP_MEMORY_FAST_PATH void aot_store32_block( std::uint32_t address, const std::uint32_t (&values)[N]) const { if (aot_try_store32_block(address, values)) return; for (std::size_t i = 0; i < N; ++i) aot_store32(address + static_cast(i * 4u), values[i]); } // Common PSP command/list builder idiom: // p = *cursor; *p = value; p = *cursor; p += 4; *cursor = p; // VCS executes this sequence extremely often in the geometry/boundary // hot traces. The direct RAM path performs one cursor read, one data // write and one cursor write. If the data write aliases the cursor, or // if either address leaves plain RAM/write-watch-safe memory, fall back // to the exact scalar sequence so the second cursor load observes any // aliasing side effect just like the generated MIPS did. PSPRECOMP_MEMORY_FAST_PATH std::uint32_t aot_append32( std::uint32_t cursor_address, std::uint32_t value, std::uint32_t *old_pointer = nullptr) const { #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (fastmem_base_ != nullptr) { #else if (fastmem_base_ != nullptr && !write_watch_enabled_) { #endif const std::uint32_t pointer = GuestMemory::read_le32(fastmem_pointer(fastmem_base_, cursor_address)); // Preserve the alias-aware overlap rule from the baseline: two // numerically different MIPS aliases may refer to the same four // physical RAM bytes. const std::uint32_t cursor_offset = ram_offset_of_fast(cursor_address); const std::uint32_t target_offset = ram_offset_of_fast(pointer); const bool target_direct = cursor_offset <= ram_limit32_ && target_offset <= ram_limit32_; const bool disjoint = target_direct && (target_offset + 3u < cursor_offset || cursor_offset + 3u < target_offset); if (disjoint) { GuestMemory::write_le32(fastmem_pointer(fastmem_base_, pointer), value); const std::uint32_t next = pointer + 4u; GuestMemory::write_le32(fastmem_pointer(fastmem_base_, cursor_address), next); if (old_pointer != nullptr) *old_pointer = pointer; return next; } } const std::uint32_t cursor_offset = ram_offset_of_fast(cursor_address); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) const bool cursor_direct = cursor_offset <= ram_limit32_; #else const bool cursor_direct = !write_watch_enabled_ && cursor_offset <= ram_limit32_; #endif if (cursor_direct) { const std::uint32_t pointer = GuestMemory::read_le32(ram_data_ + cursor_offset); const std::uint32_t target_offset = ram_offset_of_fast(pointer); const bool target_direct = target_offset <= ram_limit32_; const bool disjoint = target_direct && (target_offset + 3u < cursor_offset || cursor_offset + 3u < target_offset); if (disjoint) { GuestMemory::write_le32(ram_data_ + target_offset, value); const std::uint32_t next = pointer + 4u; GuestMemory::write_le32(ram_data_ + cursor_offset, next); if (old_pointer != nullptr) *old_pointer = pointer; return next; } } const std::uint32_t pointer = aot_load32(cursor_address); if (old_pointer != nullptr) *old_pointer = pointer; aot_store32(pointer, value); const std::uint32_t reloaded = aot_load32(cursor_address); const std::uint32_t next = reloaded + 4u; aot_store32(cursor_address, next); return next; } PSPRECOMP_MEMORY_FAST_PATH std::uint32_t aot_advance32( std::uint32_t cursor_address) const { #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (fastmem_base_ != nullptr) { #else if (fastmem_base_ != nullptr && !write_watch_enabled_) { #endif const std::uint32_t next = GuestMemory::read_le32(fastmem_pointer(fastmem_base_, cursor_address)) + 4u; GuestMemory::write_le32(fastmem_pointer(fastmem_base_, cursor_address), next); return next; } const std::uint32_t cursor_offset = ram_offset_of_fast(cursor_address); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (cursor_offset <= ram_limit32_) { #else if (!write_watch_enabled_ && cursor_offset <= ram_limit32_) { #endif const std::uint32_t next = GuestMemory::read_le32(ram_data_ + cursor_offset) + 4u; GuestMemory::write_le32(ram_data_ + cursor_offset, next); return next; } const std::uint32_t next = aot_load32(cursor_address) + 4u; aot_store32(cursor_address, next); return next; } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint32_t aot_load_word_left( std::uint32_t address, std::uint32_t existing) const { const std::uint32_t shift = (address & 3u) * 8u; const std::uint32_t memory_word = aot_load32(address & ~3u); return (existing & (0x00FFFFFFu >> shift)) | (memory_word << (24u - shift)); } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint32_t aot_load_word_right( std::uint32_t address, std::uint32_t existing) const { const std::uint32_t shift = (address & 3u) * 8u; const std::uint32_t memory_word = aot_load32(address & ~3u); return (existing & (0xFFFFFF00u << (24u - shift))) | (memory_word >> shift); } PSPRECOMP_MEMORY_FAST_PATH void aot_store_word_left( std::uint32_t address, std::uint32_t value) const { const std::uint32_t shift = (address & 3u) * 8u; const std::uint32_t aligned = address & ~3u; const std::uint32_t memory_word = aot_load32(aligned); aot_store32(aligned, (value >> (24u - shift)) | (memory_word & (0xFFFFFF00u << shift))); } PSPRECOMP_MEMORY_FAST_PATH void aot_store_word_right( std::uint32_t address, std::uint32_t value) const { const std::uint32_t shift = (address & 3u) * 8u; const std::uint32_t aligned = address & ~3u; const std::uint32_t memory_word = aot_load32(aligned); aot_store32(aligned, (value << shift) | (memory_word & (0x00FFFFFFu >> (24u - shift)))); } private: friend class GuestMemory; AotFastView(GuestMemory *owner, std::uint8_t *ram_data, std::uint32_t limit8, std::uint32_t limit16, std::uint32_t limit32, bool write_watch, std::uint8_t *fastmem_base) noexcept : owner_(owner), ram_data_(ram_data), ram_limit8_(limit8), ram_limit16_(limit16), ram_limit32_(limit32), write_watch_enabled_(write_watch), fastmem_base_(fastmem_base) {} [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH static constexpr std::uint32_t ram_offset_of_fast( std::uint32_t address) noexcept { return (address & 0x1FFFFFFFu) - GuestMemory::kPhysicalBase; } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH static std::uint8_t *fastmem_pointer( std::uint8_t *base, std::uint32_t address) noexcept { return reinterpret_cast( reinterpret_cast(base) + static_cast(address)); } GuestMemory *owner_{}; std::uint8_t *ram_data_{}; std::uint32_t ram_limit8_{}; std::uint32_t ram_limit16_{}; std::uint32_t ram_limit32_{}; bool write_watch_enabled_{}; std::uint8_t *fastmem_base_{}; public: [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH bool direct_fastmem_enabled() const noexcept { return fastmem_base_ != nullptr; } }; [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH AotFastView aot_fast_view() noexcept { return AotFastView(this, ram_data_, ram_limit8_, ram_limit16_, ram_limit32_, write_watch_enabled_, direct_fastmem_base_); } // Fast paths used only by statically generated AOT code. They retain // strict fallback behavior for invalid/cross-boundary accesses and write watches. // // These are defined inline because the generated units are separate // translation units linking against psprecomp_core without LTCG: an // out-of-line accessor turned every guest load and store into a real call, // and roughly a third of the translated instructions are memory accesses. // The inline body covers only main RAM, which is where the overwhelming // majority of guest traffic goes. EDRAM, out-of-range and write-watched // accesses fall through to the out-of-line helpers, which keep exactly the // behavior these functions had when they lived entirely in the .cpp. [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint8_t aot_load8(std::uint32_t address) const { const std::uint32_t offset = ram_offset_of(address); if (offset <= ram_limit8_) return ram_data_[offset]; return aot_load8_slow(address); } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint16_t aot_load16(std::uint32_t address) const { const std::uint32_t offset = ram_offset_of(address); if (offset <= ram_limit16_) return read_le16(ram_data_ + offset); return aot_load16_slow(address); } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH std::uint32_t aot_load32(std::uint32_t address) const { const std::uint32_t offset = ram_offset_of(address); if (offset <= ram_limit32_) return read_le32(ram_data_ + offset); return aot_load32_slow(address); } [[nodiscard]] std::uint32_t aot_load_word_left(std::uint32_t address, std::uint32_t existing) const; [[nodiscard]] std::uint32_t aot_load_word_right(std::uint32_t address, std::uint32_t existing) const; PSPRECOMP_MEMORY_FAST_PATH void aot_store8(std::uint32_t address, std::uint8_t value) { const std::uint32_t offset = ram_offset_of(address); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (offset <= ram_limit8_) { #else if (!write_watch_enabled_ && offset <= ram_limit8_) { #endif ram_data_[offset] = value; return; } aot_store8_slow(address, value); } PSPRECOMP_MEMORY_FAST_PATH void aot_store16(std::uint32_t address, std::uint16_t value) { const std::uint32_t offset = ram_offset_of(address); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (offset <= ram_limit16_) { #else if (!write_watch_enabled_ && offset <= ram_limit16_) { #endif write_le16(ram_data_ + offset, value); return; } aot_store16_slow(address, value); } PSPRECOMP_MEMORY_FAST_PATH void aot_store32(std::uint32_t address, std::uint32_t value) { const std::uint32_t offset = ram_offset_of(address); #if defined(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH) if (offset <= ram_limit32_) { #else if (!write_watch_enabled_ && offset <= ram_limit32_) { #endif write_le32(ram_data_ + offset, value); return; } aot_store32_slow(address, value); } void aot_store_word_left(std::uint32_t address, std::uint32_t value); void aot_store_word_right(std::uint32_t address, std::uint32_t value); // Generic block access used by code generators for LV.Q/SV.Q. Profiles // with a shared AotFastView lower these calls to their cached/direct view; // manual generated fixtures retain the exact checked scalar semantics. template PSPRECOMP_MEMORY_FAST_PATH void aot_load32_block( std::uint32_t address, std::uint32_t (&values)[N]) const { static_assert(N != 0u); for (std::size_t i = 0; i < N; ++i) values[i] = aot_load32(address + static_cast(i * 4u)); } template PSPRECOMP_MEMORY_FAST_PATH void aot_store32_block( std::uint32_t address, const std::uint32_t (&values)[N]) { static_assert(N != 0u); for (std::size_t i = 0; i < N; ++i) aot_store32(address + static_cast(i * 4u), values[i]); } // DEFLATE/LZ-style forward overlap copy. Unlike memmove, bytes written at // the destination become immediately available as source bytes, so a short // match (for example distance=1) can expand to a long repeated run. // Generated AOT can use this for verified overlap-aware match-copy loops such as // hot guest loops instead of executing one translated load/store per byte. void aot_copy_lz_match(std::uint32_t destination, std::uint32_t source, std::uint32_t length); // Direct pointer to `length` contiguous bytes, or nullptr when the range is // not wholly inside one region. The software rasterizer resolves the frame // and depth buffers once per primitive and then indexes raw memory, instead // of paying canonicalization plus a bounds check on every pixel. Callers // must keep the range valid; nothing here is revalidated afterwards. [[nodiscard]] std::uint8_t *raw_pointer(std::uint32_t address, std::size_t length) noexcept; [[nodiscard]] const std::uint8_t *raw_pointer(std::uint32_t address, std::size_t length) const noexcept; [[nodiscard]] std::uint8_t load8(std::uint32_t address) const; [[nodiscard]] std::uint16_t load16(std::uint32_t address) const; [[nodiscard]] std::uint32_t load32(std::uint32_t address) const; [[nodiscard]] std::uint32_t load_word_left(std::uint32_t address, std::uint32_t existing) const; [[nodiscard]] std::uint32_t load_word_right(std::uint32_t address, std::uint32_t existing) const; void store8(std::uint32_t address, std::uint8_t value); void store16(std::uint32_t address, std::uint16_t value); void store32(std::uint32_t address, std::uint32_t value); void store_word_left(std::uint32_t address, std::uint32_t value); void store_word_right(std::uint32_t address, std::uint32_t value); void memory_barrier() const noexcept; void copy_in(std::uint32_t address, std::span data); void copy_out(std::uint32_t address, std::span data) const; void zero(std::uint32_t address, std::size_t length); [[nodiscard]] std::string read_c_string(std::uint32_t address, std::size_t max_length = 256u) const; [[nodiscard]] std::span bytes() const noexcept; [[nodiscard]] std::span vram_bytes() const noexcept; [[nodiscard]] bool direct_fastmem_enabled() const noexcept { return direct_fastmem_base_ != nullptr; } [[nodiscard]] std::uintptr_t direct_fastmem_base_address() const noexcept { return reinterpret_cast(direct_fastmem_base_); } private: enum class Region { Vram, Ram }; struct ResolvedAddress { Region region; std::size_t offset; }; [[nodiscard]] ResolvedAddress resolve(std::uint32_t address, std::size_t length) const; [[nodiscard]] bool is_vram_window(std::uint32_t canonical_address) const noexcept; [[nodiscard]] std::size_t vram_offset(std::uint32_t canonical_address) const noexcept; [[nodiscard]] std::span region_bytes(Region region) const noexcept; [[nodiscard]] std::span region_bytes(Region region) noexcept; // Canonicalize and rebase in one step. An address below kPhysicalBase -- // EDRAM included -- wraps to a value far above any RAM size, so a single // unsigned compare rejects it along with every out-of-range access. [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH static std::uint32_t ram_offset_of(std::uint32_t address) noexcept { return (address & 0x1FFFFFFFu) - kPhysicalBase; } // Guest memory is little-endian, so on a little-endian host these are the // same bytes the previous per-byte assembly produced, in one access. [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH static std::uint16_t read_le16(const std::uint8_t *source) noexcept { std::uint16_t value{}; std::memcpy(&value, source, sizeof(value)); if constexpr (std::endian::native == std::endian::big) value = static_cast((value >> 8u) | (value << 8u)); return value; } [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH static std::uint32_t read_le32(const std::uint8_t *source) noexcept { std::uint32_t value{}; std::memcpy(&value, source, sizeof(value)); if constexpr (std::endian::native == std::endian::big) value = ((value >> 24u) & 0x000000FFu) | ((value >> 8u) & 0x0000FF00u) | ((value << 8u) & 0x00FF0000u) | ((value << 24u) & 0xFF000000u); return value; } PSPRECOMP_MEMORY_FAST_PATH static void write_le16(std::uint8_t *destination, std::uint16_t value) noexcept { if constexpr (std::endian::native == std::endian::big) value = static_cast((value >> 8u) | (value << 8u)); std::memcpy(destination, &value, sizeof(value)); } PSPRECOMP_MEMORY_FAST_PATH static void write_le32(std::uint8_t *destination, std::uint32_t value) noexcept { if constexpr (std::endian::native == std::endian::big) value = ((value >> 24u) & 0x000000FFu) | ((value >> 8u) & 0x0000FF00u) | ((value << 8u) & 0x00FF0000u) | ((value << 24u) & 0xFF000000u); std::memcpy(destination, &value, sizeof(value)); } [[nodiscard]] std::uint8_t aot_load8_slow(std::uint32_t address) const; [[nodiscard]] std::uint16_t aot_load16_slow(std::uint32_t address) const; [[nodiscard]] std::uint32_t aot_load32_slow(std::uint32_t address) const; void aot_store8_slow(std::uint32_t address, std::uint8_t value); void aot_store16_slow(std::uint32_t address, std::uint16_t value); void aot_store32_slow(std::uint32_t address, std::uint32_t value); std::vector fallback_vram_; std::vector fallback_ram_; std::uint32_t ram_size_{}; std::uint8_t *vram_data_{}; // Cached view of main RAM for the inline fast paths. The fallback vectors // resized after construction, so these stay valid for the object's life. // // Deliberately not __restrict. It was tried on the theory that aliasing // against the AllegrexContext was forcing the guest register file to spill // on every memory instruction: measured on the 1500-vblank route it moved // nothing (0.599 us/dispatch against 0.595 without it), and it is not even // sound here -- this pointer aliases bytes_ below, which other members of // this class access directly. std::uint8_t *ram_data_{}; // V7 architectural fastmem. On 64-bit Windows the same RAM/VRAM sections // are mapped at their PSP virtual aliases inside a sparse 4 GiB arena. // Generated AOT can then load/store at fastmem_base + guest_address, which // provides a sparse direct-address fast-memory model for generated AOT code. std::uint8_t *direct_fastmem_base_{}; static constexpr std::size_t kFastmemMaxViews = 40u; std::array fastmem_views_{}; std::size_t fastmem_view_count_{}; void *fastmem_ram_mapping_{}; void *fastmem_vram_mapping_{}; [[nodiscard]] bool initialize_direct_fastmem(std::uint32_t size_bytes) noexcept; void shutdown_direct_fastmem() noexcept; std::uint32_t ram_limit8_{}; std::uint32_t ram_limit16_{}; std::uint32_t ram_limit32_{}; bool write_watch_enabled_{}; }; } // namespace psprecomp