Files
PSPRecomp/include/psprecomp/guest_memory.hpp
Jessica_Natalia b8d2bb4f09 otimizações round 13 (severo)
otimizações round 13 (severo)
2026-08-18 13:07:44 -03:00

672 lines
35 KiB
C++

#pragma once
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <span>
#include <string>
#include <vector>
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 <std::size_t N>
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<std::uint32_t>(i * 4u)));
}
}
template <std::size_t N>
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<std::uint32_t>(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 <std::size_t N>
[[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<std::uint32_t>((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<std::uint32_t>(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 <std::size_t N>
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<std::uint32_t>(i * 4u));
}
template <std::size_t N>
[[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<std::uint32_t>((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<std::uint32_t>(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 <std::size_t N>
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<std::uint32_t>(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<std::uint8_t *>(
reinterpret_cast<std::uintptr_t>(base) + static_cast<std::uintptr_t>(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 <std::size_t N>
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<std::uint32_t>(i * 4u));
}
template <std::size_t N>
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<std::uint32_t>(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<const std::uint8_t> data);
void copy_out(std::uint32_t address, std::span<std::uint8_t> 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<const std::uint8_t> bytes() const noexcept;
[[nodiscard]] std::span<const std::uint8_t> 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<std::uintptr_t>(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<const std::uint8_t> region_bytes(Region region) const noexcept;
[[nodiscard]] std::span<std::uint8_t> 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<std::uint16_t>((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<std::uint16_t>((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<std::uint8_t> fallback_vram_;
std::vector<std::uint8_t> 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<void *, kFastmemMaxViews> 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