diff --git a/include/psprecomp/guest_memory.hpp b/include/psprecomp/guest_memory.hpp index 3dc9153..3535160 100644 --- a/include/psprecomp/guest_memory.hpp +++ b/include/psprecomp/guest_memory.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -29,6 +30,7 @@ public: 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 @@ -67,21 +69,29 @@ public: class AotFastView { public: [[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_) { @@ -94,6 +104,11 @@ public: 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_) { @@ -106,6 +121,11 @@ public: 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_) { @@ -131,6 +151,15 @@ public: 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; @@ -156,6 +185,19 @@ public: 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); @@ -192,6 +234,30 @@ public: 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_; @@ -222,6 +288,16 @@ public: } 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_) { @@ -271,25 +347,36 @@ public: 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) noexcept + 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) {} + 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_); + write_watch_enabled_, direct_fastmem_base_); } // Fast paths used only by statically generated AOT code. They retain @@ -391,8 +478,12 @@ public: 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]] const std::vector &bytes() const noexcept; - [[nodiscard]] const std::vector &vram_bytes() const noexcept; + [[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 }; @@ -404,8 +495,8 @@ private: [[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]] const std::vector ®ion_bytes(Region region) const noexcept; - [[nodiscard]] std::vector ®ion_bytes(Region region) 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 @@ -450,9 +541,11 @@ private: 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 vram_; - std::vector bytes_; - // Cached view of bytes_ for the inline fast paths. Neither region is ever + 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 @@ -462,6 +555,18 @@ private: // 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_{}; diff --git a/profiles/vcs/host/main.cpp b/profiles/vcs/host/main.cpp index 73cdf62..5832662 100644 --- a/profiles/vcs/host/main.cpp +++ b/profiles/vcs/host/main.cpp @@ -177,6 +177,17 @@ int main(int argc, char **argv) { psprecomp::Elf32Image elf = psprecomp::Elf32Image::from_file(executable); psprecomp::Runtime runtime(32u * 1024u * 1024u); + { + std::ostringstream fastmem_line; + fastmem_line << "aot direct fastmem enabled=" + << (runtime.memory().direct_fastmem_enabled() ? 1 : 0); + if (runtime.memory().direct_fastmem_enabled()) + fastmem_line << " base=0x" << std::hex + << runtime.memory().direct_fastmem_base_address() << std::dec; + else + fastmem_line << " fallback=checked-memory"; + vcs::runtime_log_line(fastmem_line.str()); + } // The heavy GUESTHOT sampler is opt-in. The rolling PERF telemetry stays on, // but normal gameplay does not pay a census/timestamp branch per cross-unit edge. psprecomp::set_guest_hotspot_profile(configuration.diagnostics.guest_hotspot_profile, 8u); diff --git a/profiles/vcs/host/vcs_profile.cpp b/profiles/vcs/host/vcs_profile.cpp index cf10cea..e14220f 100644 --- a/profiles/vcs/host/vcs_profile.cpp +++ b/profiles/vcs/host/vcs_profile.cpp @@ -1413,7 +1413,7 @@ void dump_ram_if_requested(const psprecomp::GuestMemory &memory) { std::filesystem::create_directories(config.directory); std::ostringstream stem; stem << "ram_vblank_" << std::setw(6) << std::setfill('0') << display_vblank_index; - const auto write_bytes = [&](const std::filesystem::path &path, const std::vector &bytes) { + const auto write_bytes = [&](const std::filesystem::path &path, std::span bytes) { std::ofstream output(path, std::ios::binary | std::ios::trunc); if (!output) throw std::runtime_error("Unable to create RAM diagnostic dump: " + path.string()); output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); @@ -2379,7 +2379,7 @@ SavedataDisplayMetadata read_savedata_metadata_file(const std::filesystem::path return metadata; } -// Imported PSP/PPSSPP savedata directories may already contain a standard +// Imported PSP savedata directories may already contain a standard // PARAM.SFO. Read the three user-facing strings directly so pre-existing saves // can show their title/mission metadata without first being re-saved by // VCSNative. This is deliberately a tiny bounded PSF reader, not a general SFO @@ -7651,9 +7651,11 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start total_fallback += c.fallbacks; total_sample_ns += c.sampled_ns; total_sample_entries += c.sampled_entries; + const std::uint64_t estimated_us = (c.sampled_ns * 256u) / 1000u; tier2_line << ' ' << tier2_cluster_name(id) << "_e=" << c.entries << ' ' << tier2_cluster_name(id) << "_x=" - << (c.fused_tail_edges + c.fused_calls); + << (c.fused_tail_edges + c.fused_calls) + << ' ' << tier2_cluster_name(id) << "_est_us=" << estimated_us; } tier2_line << " total_entries=" << total_entries << " fused_tail=" << total_tail diff --git a/profiles/vcs/host/vcs_runtime_log.cpp b/profiles/vcs/host/vcs_runtime_log.cpp index 3f39a12..fc4a08a 100644 --- a/profiles/vcs/host/vcs_runtime_log.cpp +++ b/profiles/vcs/host/vcs_runtime_log.cpp @@ -65,7 +65,7 @@ void runtime_log_initialize(const VcsConfiguration &configuration) { return; } s.file << "VCSNative runtime log\n"; - s.file << "stage=perf-v6-entity-leaf-inline-crashfix1-2026-08-17\n"; + s.file << "stage=perf-v7-arch-fastmem-2026-08-17\n"; s.file << "config=" << configuration.source_path.string() << '\n'; s.file << "started=" << timestamp_now() << '\n'; s.file << "perf_telemetry=" << (configuration.diagnostics.perf_telemetry ? 1 : 0) @@ -80,7 +80,8 @@ void runtime_log_initialize(const VcsConfiguration &configuration) { << " unwind_fix=1 reentry_guard=1 dataflow=1 vfpu_block32=133 mem_runs=35 mem_words=287" << " append32=51 advance32=89 simd_mat4=4 simd_matvec=19" << " gpr_shadow_clusters=4 gpr_shadow_regs=24 gpr_shadow_occurrences=3461 geometry_shadow=0" - << " perf_layer=6 entity_leaf_inline=1 entity_leaf_scheduler_accounting=1 entity_leaf_resume_pc_fix=1" + << " perf_layer=7 arch_fastmem=1 aot_direct_fastmem_default=1" + << " entity_leaf_inline=1 entity_leaf_scheduler_accounting=1 entity_leaf_resume_pc_fix=1" << " ge_async_default=0 parallel_vertex_decode_default=0" << " v5_vfpu_fast_quarantined=1 native_vfpu_088b1780_v4=1" << " dx12_execute_indirect_default=0 indirect_buffer_mb=4" diff --git a/profiles/vcs/scripts/build_release_ninja.bat b/profiles/vcs/scripts/build_release_ninja.bat index b2de956..23f49f8 100644 --- a/profiles/vcs/scripts/build_release_ninja.bat +++ b/profiles/vcs/scripts/build_release_ninja.bat @@ -101,6 +101,7 @@ set "PERF_V5_SYNC_RECOVERY_STAMP=%BUILD%\.vcs_perf_v5_sync_recovery_20260817" set "PERF_V5_STABLE_RECOVERY2_STAMP=%BUILD%\.vcs_perf_v5_stable_recovery2_20260817" set "PERF_V6_ENTITY_LEAF_STAMP=%BUILD%\.vcs_perf_v6_entity_leaf_inline_20260817" set "PERF_V6_ENTITY_LEAF_FIX1_STAMP=%BUILD%\.vcs_perf_v6_entity_leaf_inline_crashfix1_20260817" +set "PERF_V7_ARCH_FASTMEM_STAMP=%BUILD%\.vcs_perf_v7_arch_fastmem_20260817" echo ================================================================ echo VCS - NINJA PERFORMANCE INCREMENTAL BUILD @@ -112,7 +113,7 @@ echo CMake: %CMAKE_EXE% echo Ninja: %NINJA_EXE% echo Ninja workers: %JOBS% echo cl.exe /MP: OFF ^(Ninja owns compile parallelism^) -echo Generated AOT: O3, cold /Ob0, measured hot /Ob3; V6 Entity leaf-inline CRASHFIX1 over V4-stable Tier2; risky async/decode quarantined +echo Generated AOT: O3, cold /Ob0, measured hot /Ob3; V7 architectural direct-fastmem over V6 CRASHFIX1; risky async/decode quarantined echo Host/core LTCG: ON echo AVX2/fast paths: ON echo ================================================================ @@ -121,7 +122,7 @@ echo [0b/7] Reapplying BOOTFIX-safe Tier-2 transforms (OPT1 semantic transforms call "%PROFILE%\APPLY_TIER2_EXTREME.bat" if errorlevel 1 goto :FAIL -echo [0b2/7] Building V6 Entity leaf-inline CRASHFIX1 over gameplay-stable V4 Tier2... +echo [0b2/7] Building V7 ARCH FASTMEM over gameplay-stable V6 CRASHFIX1... set "PYTHON3_CMD=" py -3 -c "import sys; raise SystemExit(0 if sys.version_info.major == 3 else 1)" >nul 2>&1 if not errorlevel 1 set "PYTHON3_CMD=py -3" @@ -223,6 +224,21 @@ if exist "%BUILD%" if not exist "%PERF_V6_ENTITY_LEAF_FIX1_STAMP%" ( del /s /q "%BUILD%\*vcs_runtime_log*.obj" >nul 2>&1 ) +if exist "%BUILD%" if not exist "%PERF_V7_ARCH_FASTMEM_STAMP%" ( + echo. + echo [0c-v7fastmem/7] V7 ARCH FASTMEM - one-time AOT memory-model rebuild... + rem This is intentionally not a micro hotfix: guest_memory.hpp is inlined into every + rem generated unit so all AOT objects must see the direct-fastmem address model. + rem Geometry/Boundary keep their exact V4 Tier2 source; they only recompile against + rem the new memory view. This one-time rebuild is required for a global CPU change. + del /s /q "%BUILD%\*generated_unit_*.obj" >nul 2>&1 + del /s /q "%BUILD%\*vcs_tier2_cluster*.obj" >nul 2>&1 + del /s /q "%BUILD%\*guest_memory*.obj" >nul 2>&1 + del /s /q "%BUILD%\*vcs_profile*.obj" >nul 2>&1 + del /s /q "%BUILD%\*vcs_runtime_log*.obj" >nul 2>&1 + del /s /q "%BUILD%\*main*.obj" >nul 2>&1 +) + if exist "%BUILD%" if not exist "%BOOTFIX_STAMP%" ( echo. echo [0c/7] BOOTFIX revision changed - invalidating stale .obj/.pch once... @@ -266,6 +282,7 @@ if errorlevel 1 goto :FAIL >"%PERF_V5_STABLE_RECOVERY2_STAMP%" echo VCS PERF V5 STABLE RECOVERY2 2026-08-17 >"%PERF_V6_ENTITY_LEAF_STAMP%" echo VCS PERF V6 ENTITY LEAF INLINE 2026-08-17 >"%PERF_V6_ENTITY_LEAF_FIX1_STAMP%" echo VCS PERF V6 ENTITY LEAF INLINE CRASHFIX1 2026-08-17 +>"%PERF_V7_ARCH_FASTMEM_STAMP%" echo VCS PERF V7 ARCH FASTMEM 2026-08-17 echo. echo [2b/7] Building tests and DX12 probes... diff --git a/src/guest_memory.cpp b/src/guest_memory.cpp index 092265b..54c982c 100644 --- a/src/guest_memory.cpp +++ b/src/guest_memory.cpp @@ -7,6 +7,15 @@ #include #include #include +#include + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#define WIN32_LEAN_AND_MEAN +#include +#endif namespace psprecomp { @@ -55,6 +64,14 @@ bool overlaps_watch(std::uint32_t address, std::size_t length) { static_cast(canonical_watch) < first_end; } +bool environment_enabled_default_on(const char *name) noexcept { + const char *value = std::getenv(name); + if (value == nullptr || *value == '\0') return true; + const std::string_view text(value); + return !(text == "0" || text == "off" || text == "OFF" || + text == "false" || text == "FALSE" || text == "no" || text == "NO"); +} + void log_write_watch(std::uint32_t address, std::size_t length, const char *operation, std::uint64_t old_value, std::uint64_t new_value) { if (!overlaps_watch(address, length)) return; @@ -69,21 +86,182 @@ void log_write_watch(std::uint32_t address, std::size_t length, const char *oper } } +bool GuestMemory::initialize_direct_fastmem(std::uint32_t size_bytes) noexcept { + direct_fastmem_base_ = nullptr; + fastmem_view_count_ = 0u; + fastmem_views_.fill(nullptr); + fastmem_ram_mapping_ = nullptr; + fastmem_vram_mapping_ = nullptr; + + if (!environment_enabled_default_on("PSPRECOMP_AOT_DIRECT_FASTMEM")) + return false; + +#if defined(_WIN32) && INTPTR_MAX > INT32_MAX + HANDLE ram_mapping = CreateFileMappingW( + INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0u, + static_cast(size_bytes), nullptr); + if (ram_mapping == nullptr) return false; + + HANDLE vram_mapping = CreateFileMappingW( + INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0u, + static_cast(kVramSize), nullptr); + if (vram_mapping == nullptr) { + CloseHandle(ram_mapping); + return false; + } + + // Map the exact alias model used by canonical(address): the top three bits + // are ignored, so every 0x20000000 mirror must resolve to the same physical + // bytes. VRAM additionally has four 2 MiB mirrors inside its 8 MiB window. + // No 4 GiB reservation is needed; only the 40 live sparse views consume VA. + const auto clear_attempt = [&]() noexcept { + for (std::size_t i = 0u; i < fastmem_view_count_; ++i) { + if (fastmem_views_[i] != nullptr) UnmapViewOfFile(fastmem_views_[i]); + fastmem_views_[i] = nullptr; + } + fastmem_view_count_ = 0u; + }; + + const auto map_exact = [&](HANDLE mapping, std::uintptr_t host_address, + std::size_t bytes) noexcept -> bool { + void *const requested = reinterpret_cast(host_address); + void *const view = MapViewOfFileEx(mapping, FILE_MAP_ALL_ACCESS, 0u, 0u, + bytes, requested); + if (view != requested) { + if (view != nullptr) UnmapViewOfFile(view); + return false; + } + if (fastmem_view_count_ >= fastmem_views_.size()) { + UnmapViewOfFile(view); + return false; + } + fastmem_views_[fastmem_view_count_++] = view; + return true; + }; + + // High, 64 KiB-aligned bases keep the sparse PSP 4 GiB window away from + // ordinary executable/heap allocations. Try several independent 1 TiB + // slots so ASLR or another mapping cannot make fastmem boot-critical. + constexpr std::uintptr_t kFirstCandidate = UINT64_C(0x0000040000000000); + constexpr std::uintptr_t kCandidateStep = UINT64_C(0x0000010000000000); + constexpr std::size_t kCandidateCount = 24u; + + bool mapped = false; + for (std::size_t attempt = 0u; attempt < kCandidateCount && !mapped; ++attempt) { + clear_attempt(); + const std::uintptr_t base = kFirstCandidate + kCandidateStep * attempt; + bool ok = true; + + for (std::uint32_t alias = 0u; alias < 8u && ok; ++alias) { + const std::uint32_t guest = kPhysicalBase + alias * 0x20000000u; + ok = map_exact(ram_mapping, base + guest, size_bytes); + } + for (std::uint32_t alias = 0u; alias < 8u && ok; ++alias) { + for (std::uint32_t mirror = 0u; mirror < kVramMirrorCount && ok; ++mirror) { + const std::uint32_t guest = kVramPhysicalBase + mirror * kVramSize + + alias * 0x20000000u; + ok = map_exact(vram_mapping, base + guest, kVramSize); + } + } + + if (ok) { + // Verify that the OS really gave us coherent aliases before any + // guest data is loaded. This turns a broken/partial mapping into a + // clean fallback rather than latent guest-memory corruption. + auto *const probe_base = reinterpret_cast(base); + const std::uint32_t ram_probe_offset = size_bytes - 1u; + probe_base[kPhysicalBase + ram_probe_offset] = 0x5Au; + for (std::uint32_t alias = 0u; alias < 8u && ok; ++alias) { + const std::uint32_t guest = kPhysicalBase + alias * 0x20000000u; + ok = probe_base[guest + ram_probe_offset] == 0x5Au; + } + probe_base[kPhysicalBase + ram_probe_offset] = 0u; + + const std::uint32_t vram_probe_offset = kVramSize - 1u; + probe_base[kVramPhysicalBase + vram_probe_offset] = 0xA5u; + for (std::uint32_t alias = 0u; alias < 8u && ok; ++alias) { + for (std::uint32_t mirror = 0u; mirror < kVramMirrorCount && ok; ++mirror) { + const std::uint32_t guest = kVramPhysicalBase + mirror * kVramSize + + alias * 0x20000000u; + ok = probe_base[guest + vram_probe_offset] == 0xA5u; + } + } + probe_base[kVramPhysicalBase + vram_probe_offset] = 0u; + } + + if (ok) { + direct_fastmem_base_ = reinterpret_cast(base); + mapped = true; + } + } + + if (!mapped) { + clear_attempt(); + CloseHandle(vram_mapping); + CloseHandle(ram_mapping); + return false; + } + + fastmem_ram_mapping_ = ram_mapping; + fastmem_vram_mapping_ = vram_mapping; + return true; +#else + (void)size_bytes; + return false; +#endif +} + +void GuestMemory::shutdown_direct_fastmem() noexcept { +#if defined(_WIN32) && INTPTR_MAX > INT32_MAX + for (std::size_t i = 0u; i < fastmem_view_count_; ++i) { + if (fastmem_views_[i] != nullptr) UnmapViewOfFile(fastmem_views_[i]); + fastmem_views_[i] = nullptr; + } + fastmem_view_count_ = 0u; + if (fastmem_vram_mapping_ != nullptr) { + CloseHandle(static_cast(fastmem_vram_mapping_)); + fastmem_vram_mapping_ = nullptr; + } + if (fastmem_ram_mapping_ != nullptr) { + CloseHandle(static_cast(fastmem_ram_mapping_)); + fastmem_ram_mapping_ = nullptr; + } +#endif + direct_fastmem_base_ = nullptr; +} + GuestMemory::GuestMemory(std::uint32_t size_bytes) - : vram_(kVramSize, 0u), bytes_(size_bytes, 0u), write_watch_enabled_(std::getenv("PSPRECOMP_WATCH_WRITE") != nullptr) { + : ram_size_(size_bytes), + write_watch_enabled_(std::getenv("PSPRECOMP_WATCH_WRITE") != nullptr) { if (size_bytes != 32u * 1024u * 1024u && size_bytes != 64u * 1024u * 1024u) { throw Error("PSP RAM size must be 32 MiB or 64 MiB"); } - // Bind the inline AOT fast paths to main RAM. bytes_ is never resized - // afterwards, and the instance is non-copyable, so this stays valid. - ram_data_ = bytes_.data(); + + if (initialize_direct_fastmem(size_bytes)) { + // These two aliases are backed by the same page-file sections as every + // other PSP mirror in the fastmem arena. Keeping the ordinary pointers + // on those mappings makes HLE/raw_pointer/ELF loading coherent with the + // generated AOT direct-address path without a shadow copy. + vram_data_ = direct_fastmem_base_ + kVramPhysicalBase; + ram_data_ = direct_fastmem_base_ + kPhysicalBase; + } else { + fallback_vram_.assign(kVramSize, 0u); + fallback_ram_.assign(size_bytes, 0u); + vram_data_ = fallback_vram_.data(); + ram_data_ = fallback_ram_.data(); + } + ram_limit8_ = size_bytes - 1u; ram_limit16_ = size_bytes - 2u; ram_limit32_ = size_bytes - 4u; } -std::uint32_t GuestMemory::size() const noexcept { return static_cast(bytes_.size()); } -std::uint32_t GuestMemory::vram_size() const noexcept { return static_cast(vram_.size()); } +GuestMemory::~GuestMemory() { + shutdown_direct_fastmem(); +} + +std::uint32_t GuestMemory::size() const noexcept { return ram_size_; } +std::uint32_t GuestMemory::vram_size() const noexcept { return kVramSize; } bool GuestMemory::is_vram_window(std::uint32_t canonical_address) const noexcept { return canonical_address >= kVramPhysicalBase && @@ -99,7 +277,7 @@ bool GuestMemory::contains(std::uint32_t address, std::size_t length) const noex const std::uint64_t end = static_cast(c) + static_cast(length); if (is_vram_window(c) && end <= static_cast(kVramPhysicalBase) + kVramAddressSpan) return true; - if (c >= kPhysicalBase && end <= static_cast(kPhysicalBase) + bytes_.size()) + if (c >= kPhysicalBase && end <= static_cast(kPhysicalBase) + ram_size_) return true; return false; } @@ -114,11 +292,15 @@ GuestMemory::ResolvedAddress GuestMemory::resolve(std::uint32_t address, std::si return {Region::Ram, static_cast(c - kPhysicalBase)}; } -const std::vector &GuestMemory::region_bytes(Region region) const noexcept { - return region == Region::Vram ? vram_ : bytes_; +std::span GuestMemory::region_bytes(Region region) const noexcept { + return region == Region::Vram + ? std::span(vram_data_, kVramSize) + : std::span(ram_data_, ram_size_); } -std::vector &GuestMemory::region_bytes(Region region) noexcept { - return region == Region::Vram ? vram_ : bytes_; +std::span GuestMemory::region_bytes(Region region) noexcept { + return region == Region::Vram + ? std::span(vram_data_, kVramSize) + : std::span(ram_data_, ram_size_); } // The `_slow` bodies below are the original aot_* implementations, reached only @@ -126,9 +308,9 @@ std::vector &GuestMemory::region_bytes(Region region) noexcept { // an out-of-range address, a region-crossing width, or an armed write watch. std::uint8_t GuestMemory::aot_load8_slow(std::uint32_t address) const { const std::uint32_t c = canonical(address); - if (is_vram_window(c)) return vram_[vram_offset(c)]; - if (c >= kPhysicalBase && c - kPhysicalBase < bytes_.size()) - return bytes_[static_cast(c - kPhysicalBase)]; + if (is_vram_window(c)) return vram_data_[vram_offset(c)]; + if (c >= kPhysicalBase && c - kPhysicalBase < ram_size_) + return ram_data_[static_cast(c - kPhysicalBase)]; return load8(address); } @@ -136,34 +318,37 @@ std::uint16_t GuestMemory::aot_load16_slow(std::uint32_t address) const { const std::uint32_t c = canonical(address); if (is_vram_window(c)) { const std::size_t offset = vram_offset(c); - if (offset + 2u <= vram_.size()) - return static_cast(vram_[offset]) | - static_cast(static_cast(vram_[offset + 1u]) << 8u); + if (offset + 2u <= static_cast(kVramSize)) + return static_cast(vram_data_[offset]) | + static_cast(static_cast(vram_data_[offset + 1u]) << 8u); } else if (c >= kPhysicalBase) { const std::size_t offset = static_cast(c - kPhysicalBase); - if (offset + 2u <= bytes_.size()) - return static_cast(bytes_[offset]) | - static_cast(static_cast(bytes_[offset + 1u]) << 8u); + if (offset + 2u <= ram_size_) + return static_cast(ram_data_[offset]) | + static_cast(static_cast(ram_data_[offset + 1u]) << 8u); } return load16(address); } std::uint32_t GuestMemory::aot_load32_slow(std::uint32_t address) const { const std::uint32_t c = canonical(address); - const std::vector *data = nullptr; + const std::uint8_t *data = nullptr; + std::size_t data_size = 0u; std::size_t offset = 0u; if (is_vram_window(c)) { - data = &vram_; + data = vram_data_; + data_size = kVramSize; offset = vram_offset(c); } else if (c >= kPhysicalBase) { - data = &bytes_; + data = ram_data_; + data_size = ram_size_; offset = static_cast(c - kPhysicalBase); } - if (data != nullptr && offset + 4u <= data->size()) { - return static_cast((*data)[offset]) | - (static_cast((*data)[offset + 1u]) << 8u) | - (static_cast((*data)[offset + 2u]) << 16u) | - (static_cast((*data)[offset + 3u]) << 24u); + if (data != nullptr && offset + 4u <= data_size) { + return static_cast(data[offset]) | + (static_cast(data[offset + 1u]) << 8u) | + (static_cast(data[offset + 2u]) << 16u) | + (static_cast(data[offset + 3u]) << 24u); } return load32(address); } @@ -182,9 +367,9 @@ std::uint32_t GuestMemory::aot_load_word_right(std::uint32_t address, std::uint3 void GuestMemory::aot_store8_slow(std::uint32_t address, std::uint8_t value) { if (write_watch_enabled_) { store8(address, value); return; } const std::uint32_t c = canonical(address); - if (is_vram_window(c)) { vram_[vram_offset(c)] = value; return; } - if (c >= kPhysicalBase && c - kPhysicalBase < bytes_.size()) { - bytes_[static_cast(c - kPhysicalBase)] = value; + if (is_vram_window(c)) { vram_data_[vram_offset(c)] = value; return; } + if (c >= kPhysicalBase && c - kPhysicalBase < ram_size_) { + ram_data_[static_cast(c - kPhysicalBase)] = value; return; } store8(address, value); @@ -192,13 +377,14 @@ void GuestMemory::aot_store8_slow(std::uint32_t address, std::uint8_t value) { void GuestMemory::aot_store16_slow(std::uint32_t address, std::uint16_t value) { if (write_watch_enabled_) { store16(address, value); return; } const std::uint32_t c = canonical(address); - std::vector *data = nullptr; + std::uint8_t *data = nullptr; + std::size_t data_size = 0u; std::size_t offset = 0u; - if (is_vram_window(c)) { data = &vram_; offset = vram_offset(c); } - else if (c >= kPhysicalBase) { data = &bytes_; offset = static_cast(c - kPhysicalBase); } - if (data != nullptr && offset + 2u <= data->size()) { - (*data)[offset] = static_cast(value & 0xFFu); - (*data)[offset + 1u] = static_cast((value >> 8u) & 0xFFu); + if (is_vram_window(c)) { data = vram_data_; data_size = kVramSize; offset = vram_offset(c); } + else if (c >= kPhysicalBase) { data = ram_data_; data_size = ram_size_; offset = static_cast(c - kPhysicalBase); } + if (data != nullptr && offset + 2u <= data_size) { + data[offset] = static_cast(value & 0xFFu); + data[offset + 1u] = static_cast((value >> 8u) & 0xFFu); return; } store16(address, value); @@ -206,15 +392,16 @@ void GuestMemory::aot_store16_slow(std::uint32_t address, std::uint16_t value) { void GuestMemory::aot_store32_slow(std::uint32_t address, std::uint32_t value) { if (write_watch_enabled_) { store32(address, value); return; } const std::uint32_t c = canonical(address); - std::vector *data = nullptr; + std::uint8_t *data = nullptr; + std::size_t data_size = 0u; std::size_t offset = 0u; - if (is_vram_window(c)) { data = &vram_; offset = vram_offset(c); } - else if (c >= kPhysicalBase) { data = &bytes_; offset = static_cast(c - kPhysicalBase); } - if (data != nullptr && offset + 4u <= data->size()) { - (*data)[offset] = static_cast(value & 0xFFu); - (*data)[offset + 1u] = static_cast((value >> 8u) & 0xFFu); - (*data)[offset + 2u] = static_cast((value >> 16u) & 0xFFu); - (*data)[offset + 3u] = static_cast((value >> 24u) & 0xFFu); + if (is_vram_window(c)) { data = vram_data_; data_size = kVramSize; offset = vram_offset(c); } + else if (c >= kPhysicalBase) { data = ram_data_; data_size = ram_size_; offset = static_cast(c - kPhysicalBase); } + if (data != nullptr && offset + 4u <= data_size) { + data[offset] = static_cast(value & 0xFFu); + data[offset + 1u] = static_cast((value >> 8u) & 0xFFu); + data[offset + 2u] = static_cast((value >> 16u) & 0xFFu); + data[offset + 3u] = static_cast((value >> 24u) & 0xFFu); return; } store32(address, value); @@ -259,7 +446,7 @@ void GuestMemory::aot_copy_lz_match(std::uint32_t destination, std::uint32_t sou return; } - auto &data = region_bytes(destination_resolved.region); + auto data = region_bytes(destination_resolved.region); if (destination_resolved.offset + length > data.size() || source_resolved.offset + length > data.size() || source_resolved.offset >= destination_resolved.offset) { @@ -276,10 +463,10 @@ void GuestMemory::aot_copy_lz_match(std::uint32_t destination, std::uint32_t sou // the already produced prefix in geometrically growing non-overlapping // chunks. This is equivalent to the guest's forward byte loop, including // distance=1 runs, but completes in O(log(length)) host copies. - std::size_t copied = std::min(distance, total); + std::size_t copied = (std::min)(distance, total); std::memcpy(data.data() + destination_offset, data.data() + source_offset, copied); while (copied < total) { - const std::size_t chunk = std::min(copied, total - copied); + const std::size_t chunk = (std::min)(copied, total - copied); std::memcpy(data.data() + destination_offset + copied, data.data() + destination_offset, chunk); copied += chunk; } @@ -296,12 +483,12 @@ const std::uint8_t *GuestMemory::raw_pointer(std::uint32_t address, std::size_t const std::size_t offset = vram_offset(c); // A run that would wrap past the end of the 2 MiB EDRAM image is not // contiguous in host memory even though it is legal in guest space. - if (offset + length <= vram_.size()) return vram_.data() + offset; + if (offset + length <= static_cast(kVramSize)) return vram_data_ + offset; return nullptr; } if (c < kPhysicalBase) return nullptr; const std::size_t offset = static_cast(c - kPhysicalBase); - if (offset + length <= bytes_.size()) return bytes_.data() + offset; + if (offset + length <= ram_size_) return ram_data_ + offset; return nullptr; } @@ -331,7 +518,7 @@ std::uint32_t GuestMemory::load_word_right(std::uint32_t address, std::uint32_t } void GuestMemory::store8(std::uint32_t address, std::uint8_t value) { const auto r = resolve(address, 1u); - auto &data = region_bytes(r.region); + auto data = region_bytes(r.region); const std::uint8_t old = data[r.offset]; log_write_watch(address, 1u, "store8", old, value); data[r.offset] = value; @@ -381,8 +568,8 @@ void GuestMemory::copy_in(std::uint32_t address, std::span s while (copied < source.size()) { const std::uint32_t current = address + static_cast(copied); const auto r = resolve(current, 1u); - auto &data = region_bytes(r.region); - const std::size_t chunk = std::min(source.size() - copied, data.size() - r.offset); + auto data = region_bytes(r.region); + const std::size_t chunk = (std::min)(source.size() - copied, data.size() - r.offset); std::copy_n(source.begin() + static_cast(copied), chunk, data.begin() + static_cast(r.offset)); copied += chunk; @@ -395,8 +582,8 @@ void GuestMemory::copy_out(std::uint32_t address, std::span destin while (copied < destination.size()) { const std::uint32_t current = address + static_cast(copied); const auto r = resolve(current, 1u); - const auto &data = region_bytes(r.region); - const std::size_t chunk = std::min(destination.size() - copied, data.size() - r.offset); + const auto data = region_bytes(r.region); + const std::size_t chunk = (std::min)(destination.size() - copied, data.size() - r.offset); std::copy_n(data.begin() + static_cast(r.offset), chunk, destination.begin() + static_cast(copied)); copied += chunk; @@ -410,8 +597,8 @@ void GuestMemory::zero(std::uint32_t address, std::size_t length) { while (cleared < length) { const std::uint32_t current = address + static_cast(cleared); const auto r = resolve(current, 1u); - auto &data = region_bytes(r.region); - const std::size_t chunk = std::min(length - cleared, data.size() - r.offset); + auto data = region_bytes(r.region); + const std::size_t chunk = (std::min)(length - cleared, data.size() - r.offset); std::fill_n(data.begin() + static_cast(r.offset), chunk, 0u); cleared += chunk; } @@ -426,7 +613,11 @@ std::string GuestMemory::read_c_string(std::uint32_t address, std::size_t max_le } throw Error("Unterminated guest string at " + hex32(address)); } -const std::vector &GuestMemory::bytes() const noexcept { return bytes_; } -const std::vector &GuestMemory::vram_bytes() const noexcept { return vram_; } +std::span GuestMemory::bytes() const noexcept { + return {ram_data_, ram_size_}; +} +std::span GuestMemory::vram_bytes() const noexcept { + return {vram_data_, kVramSize}; +} } // namespace psprecomp