Files
ARMSX2/pcsx2/Memory.cpp
T
Brandon 45368481be iOS Hybrid JIT Safety, No-JIT Fallback, and Interpreter Performance
# iOS Hybrid JIT Safety, No-JIT Fallback, and Interpreter Performance

## Summary

This pull request preserves the current master JIT path while making iOS game boot reliable when JIT is unavailable, revoked, or unable to allocate executable code memory. Instead of continuing into recompiler-only code with missing mappings, ARMSX2 selects the existing interpreter providers and disables only the facilities which require generated native code.

It also adds a conservative predecoded EE interpreter block cache and enables link-time optimization for the iOS PCSX2 core. Together, these changes improve the practical No-JIT path without changing the normal JIT execution architecture.

Base revision after fetching the latest master:

`1a5fc1c3731dd98f84eb62c7d9ac948241743ce7`

The working branch and `origin/master` were already identical at that revision, so no commit replay or conflict resolution was required.

## Problems addressed

### Booting without JIT could crash or remain on a black screen

The emulator previously continued through code paths which assumed executable code memory existed. If iOS had not granted JIT, had revoked it, or the executable mapping could not be allocated, null or unavailable code-cache memory could still reach:

- The EE and IOP recompilers.
- microVU0 and microVU1.
- Generated VIF unpackers.
- The software GS scanline JIT.
- Fastmem setup.
- MTVU paths which depend on generated VIF execution.

The new capability-driven fallback allows the VM to start using existing interpreter implementations instead of invoking those incompatible providers.

## Performance impact

### No-JIT EE execution

The user-observed EE time changed from **37.55 ms to 30.55 ms** after the conservative cache and ThinLTO baseline was introduced.

That is:

- **7.00 ms less EE time** in the observed workload.
- Approximately **18.6% lower EE processing time** for that workload.
- Approximately **1.23x the previous EE throughput**, if all other conditions are equal.

This is an observed device result supplied with the change, not a benchmark performed during this packaging step. Results remain game-, scene-, device-, thermal-, and settings-dependent.

The fixed block cache uses approximately 1 MiB of static memory on 64-bit builds. It trades that bounded allocation and per-block RAM validation for fewer repeated instruction decodes.

### Normal JIT gameplay

The EE block cache is not called from the JIT's `recExecute()` path. No cache lookup, validation, or interpreter callback is added per JIT-generated instruction.

The validation mutex is used only during idle JIT checking, startup transitions, backend switching, and teardown. The keep-alive timer is stopped before gameplay.

ThinLTO may provide a small native-core improvement, but no specific steady-state JIT gain is claimed.

### JIT validation could race code-memory teardown

Master's JIT keep-alive validates more than the `CS_DEBUGGED` process flag: it checks the active writable code mapping by writing a temporary canary byte, reading it back, and restoring the original byte.

That stronger validation is retained. The problem addressed here is synchronization: canceling a dispatch source prevents future callbacks but does not wait for a callback which is already executing. Without a shared lifetime boundary, a callback could retain the mapping address while another path dismantled the code cache.

### Persistent workers cannot safely change backend in place

A JIT worker owns executable mappings and initialized recompiler providers. An interpreter worker deliberately owns neither. Reusing one worker as the other backend can leave incompatible memory and provider state.

Backend changes now perform a complete worker teardown and recreation rather than attempting to mutate an initialized worker.

### The instruction-by-instruction EE interpreter repeated decode work

No-JIT EE execution previously fetched and decoded every instruction every time it executed. Repeated loops therefore paid the same opcode lookup cost continuously.

A bounded block cache now stores short, validated sequences of instruction words and their predecoded opcode descriptors.

## JIT behavior preserved from master

The normal JIT path remains the preferred path whenever executable code memory is available.

- Fresh-launch worker preparation remains intact.
- The persistent CPU worker and condition-variable wait model remain intact.
- The configured iOS JIT script protocol is still applied by the existing gate.
- `CS_DEBUGGED` validation remains intact.
- Master's writable code-memory canary remains intact.
- The 12-second idle validation interval is unchanged.
- The canary is skipped while the VM or CPU initialization is active.
- JIT-enabled sessions retain EE, IOP, VU0, VU1, VIF, software-GS, fastmem, and MTVU acceleration as configured.
- Temporary No-JIT fallback does not overwrite the user's saved recompiler preferences.
- No interpreter cache lookup is performed by `recExecute()`.

## Hybrid JIT validation synchronization

`DarwinMisc` now owns a private mutex covering the validation canary and executable mapping lifetime.

The synchronization sequence is:

1. Mapping address, size, and alias offset are published while holding the validation mutex.
2. `ValidateJITAlive()` first checks whether CPU work is active.
3. An idle validation takes the mutex before reading or touching the mapping.
4. The original byte and page protection are restored before releasing the mutex.
5. Mapping teardown takes the same mutex, clears the published mapping state, and only then unmaps the aliases.

`WaitForJITValidation()` gives the iOS worker an explicit drain point:

- Before gameplay, the worker marks itself active, stops future periodic validation, and waits for any callback which already passed the idle check.
- Before backend teardown, it stops validation and waits for the same lifetime boundary before `CPUThreadShutdown()` releases executable memory.

The keep-alive dispatch source has separate ownership synchronization so concurrent start/stop operations do not create multiple timers or race timer release.

Interpreter-only sessions never start the keep-alive timer because they have no executable mapping and no JIT grant to preserve.

## No-JIT boot flow

When JIT is unavailable, the boot-scoped runtime flow is:

1. The iOS JIT gate requests interpreter mode for the new CPU worker.
2. VM data memory is allocated normally.
3. Executable code memory is omitted.
4. Recompiler providers are not reserved or initialized.
5. Runtime configuration is clamped to the providers which actually exist.
6. EE and IOP select their interpreter implementations.
7. VU0 and VU1 select their interpreter implementations.
8. VIF uses precompiled unpack functions.
9. Software GS uses its C setup, scanline, and edge functions.
10. Fastmem and MTVU are disabled for that boot.
11. The JIT keep-alive timer is not created.

If the initial JIT gate succeeds but executable allocation subsequently fails, allocation now falls back to the same interpreter path instead of aborting the boot.

These are runtime capability overrides. Saved JIT, fastmem, VU, and MTVU preferences are not rewritten, so a later worker created with valid JIT access can use the configured accelerated path again.

## Safe provider and memory handling

`SysMemory` exposes two explicit capabilities:

- `IsAllocated()` distinguishes an initialized VM memory map from early settings loading.
- `HasCodeMemory()` identifies whether native code generators can be used.

This distinction prevents startup settings loading from being mistaken for a No-JIT VM while allowing every code-generation path to gate itself after allocation.

Provider initialization is tracked explicitly. Shutdown and cache-reset paths therefore avoid touching recompilers which were never constructed.

Releasing the memory map also clears the recorded JIT address range, preventing later diagnostics or validation from treating released memory as live code.

## VIF and MTVU fallback

Generated VIF unpackers share the executable VM allocation. `CanUseVifDynarec()` now describes the actual runtime capability: generated VIF support must be compiled in and executable code memory must exist.

The capability check covers:

- Standard VIF unpack dispatch.
- MTVU unpack dispatch.
- VIF reset.
- Mode-zero unpack tables.

When generated mode-zero entries do not exist, VIF uses the existing precompiled C function table instead of dereferencing an uninitialized generated-function pointer.

MTVU is disabled in interpreter-only mode because its VIF path depends on generated unpack execution.

## Software GS fallback

The software renderer no longer resets, queries, or emits into its native scanline cache when code memory is absent.

Interpreter-only execution selects:

- `CSetupPrim`
- `CDrawScanline`
- `CDrawEdge` when antialiasing requires it

The normal generated software renderer remains unchanged when JIT memory exists, and the Metal hardware renderer is not replaced by this fallback.

## Fastmem behavior

No-JIT execution does not emit fastmem accesses, so interpreter-only sessions skip the 4 GB virtual-address reservation and force fastmem off for that boot.

Settings reloads cannot silently re-enable fastmem against a missing reservation. The existing iOS behavior for a genuine fastmem allocation failure also remains: the VM continues without fastmem instead of terminating startup.

## Persistent-worker backend switching

The CPU worker records whether it was initialized with JIT capability.

When a later boot requests a different backend:

1. The current worker receives an exit request.
2. Idle validation is stopped and drained.
3. `CPUThreadShutdown()` releases the matching providers and mappings.
4. The exiting worker clears its creation/backend state and notifies waiters.
5. The caller waits for actual teardown rather than relying on a fixed sleep.
6. A new worker is created for the requested backend.

This prevents overlapping workers, duplicate memory reservations, and reuse of stale JIT mappings.

## Conservative EE interpreter block cache

The new `no-jit-improvements` component accelerates only the EE interpreter.

### Cache structure

- 4,096 direct-mapped cache slots.
- Up to 16 EE instructions per slot.
- Fixed process-lifetime allocation; no heap allocation occurs in the execution loop.
- Each entry stores the original instruction words and pointers to their decoded opcode descriptors.
- Cache entries are aligned to reduce false sharing and keep slot access predictable.

### Execution

On a cache hit, the interpreter reuses the decoded opcode descriptors and executes the short sequence through the existing interpreter functions. It stops immediately if:

- The program counter no longer matches the expected instruction.
- An exception or other control transfer changes the PC.
- A branch boundary is reached.

If the address cannot be cached safely, the original one-instruction `execI()` path is used.

### Correctness safeguards

- Instruction bytes are compared with current emulated RAM before every reuse.
- Blocks terminate after branches, stores, and COP0 instructions which can change memory or address-translation state.
- EE cache-clear notifications eagerly invalidate overlapping entries.
- A complete reset occurs at interpreter reset and shutdown.
- Large or wrapping invalidation ranges trigger a complete cache reset.
- Debug/development oracle configurations retain the original instruction path.
- The cache is disabled when EE cache emulation requires an instruction view which differs from RAM.

This is intentionally conservative. It does not add direct-threaded dispatch, superinstructions, or new MMI/VU NEON implementations.

## ThinLTO for the iOS core

The iOS Xcode generation script now enables `LTO_PCSX2_CORE`.

PCSX2's existing CMake support applies interprocedural optimization to the selected core source set. This can reduce native call and optimization boundaries in both JIT-enabled and interpreter builds.

ThinLTO does not alter the runtime-generated EE JIT blocks and is not expected to transform JIT performance. Its largest relevance here is reducing host-side overhead around the interpreter and core helpers.

## iOS build stability

The generated Xcode target uses per-file optimized Swift compilation with batch mode disabled. This limits peak compiler memory use for the large SwiftUI source set while preserving optimized Release emission.

This is a build-time setting and adds no runtime work.

### Idle menu

The existing 12-second validation cadence is unchanged. Each idle check validates one mapping byte, restores it, and exits. Interpreter-only workers create no validation timer.

### No-JIT resource use

Interpreter fallback avoids resources which cannot improve No-JIT execution:

- No EE/IOP/VU native code caches.
- No generated VIF cache.
- No software-GS JIT cache.
- No 4 GB fastmem virtual reservation.
- No MTVU worker dependent on generated VIF execution.
- No JIT keep-alive source.

No-JIT remains materially slower than a valid ARM64 recompiler. The purpose is safe boot plus a measurable reduction in interpreter overhead, not parity with JIT.

## Behavior matrix

| Runtime state | EE/IOP | VU0/VU1 | Code cache | Fastmem | MTVU | VIF | SW GS | Keep-alive |
|---|---|---|---:|---:|---:|---|---|---|
| Valid JIT grant | Recompiler as configured | microVU as configured | Allocated | Configured/available | Configured | Generated | Generated | Idle only |
| No `CS_DEBUGGED` grant | Interpreter | Interpreter | None | Off | Off | Precompiled | C functions | Off |
| Executable allocation failure | Interpreter | Interpreter | None | Off | Off | Precompiled | C functions | Off |
| Active JIT gameplay | Recompiler as configured | microVU as configured | Allocated | Configured/available | Configured | Generated | Generated | Stopped |

## Files changed

### JIT lifecycle and iOS worker

- `common/Darwin/DarwinMisc.cpp`
  - Synchronizes validation with executable mapping publication and release.
  - Preserves and safely drains the writable code-memory canary.
- `common/Darwin/DarwinMisc.h`
  - Documents the interpreter capability override and exposes the validation drain API.
- `platforms/ios/app/src/main/cpp/IOS/SceneDelegate.mm`
  - Adds backend-aware persistent-worker recreation and keep-alive ownership synchronization.
- `platforms/ios/app/src/main/cpp/ios_main.mm`
  - Prevents settings repair from re-enabling recompilers during an interpreter-only boot.

### Runtime capability and No-JIT fallbacks

- `pcsx2/Memory.cpp`
- `pcsx2/Memory.h`
- `pcsx2/VMManager.cpp`
- `pcsx2/Vif_Dynarec.h`
- `pcsx2/Vif_Unpack.cpp`
- `pcsx2/MTVU.cpp`
- `pcsx2/GS/Renderers/SW/GSDrawScanline.cpp`
- `pcsx2/vtlb.cpp`
- `pcsx2/vtlb.h`

These files make executable code memory an explicit capability and gate EE, IOP, VU, VIF, software-GS, fastmem, and MTVU behavior accordingly.

### EE interpreter performance

- `pcsx2/no-jit-improvements.cpp`
- `pcsx2/no-jit-improvements.h`
- `pcsx2/Interpreter.cpp`
- `pcsx2/CMakeLists.txt`

These files implement and register the validated predecoded EE block cache.

### iOS build configuration

- `platforms/ios/scripts/generate-ios-xcode.sh`
  - Enables the existing PCSX2 core LTO target.
- `platforms/ios/app/src/main/cpp/CMakeLists.txt`
  - Uses per-file optimized Swift compilation to reduce build-time frontend memory pressure.


## Explicitly not included

The subsequently evaluated POC's are absent from this PR:

- Direct-threaded EE dispatch.
- EE superinstructions.
- New ARM64 NEON MMI implementations.
- New ARM64 NEON VU implementations.

`pcsx2/MMI.cpp` and `pcsx2/VUops.cpp` remain identical to the current master revision.

## Validation

- Fetched and compared against the latest `origin/master`.
- Confirmed the local branch and remote master resolve to the same base revision.
- Full unsigned iOS IPA build completed with `platforms/ios/scripts/build-ios-ipa.sh` before packaging.
- `git diff --check` completed without whitespace errors.
2026-07-31 15:56:20 +02:00

1460 lines
43 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
/*
RAM
---
0x00100000-0x01ffffff this is the physical address for the ram.its cached there
0x20100000-0x21ffffff uncached
0x30100000-0x31ffffff uncached & accelerated
0xa0000000-0xa1ffffff MIRROR might...???
0x80000000-0x81ffffff MIRROR might... ????
scratch pad
----------
0x70000000-0x70003fff scratch pad
BIOS
----
0x1FC00000 - 0x1FFFFFFF un-cached
0x9FC00000 - 0x9FFFFFFF cached
0xBFC00000 - 0xBFFFFFFF un-cached
*/
#include "DEV9/DEV9.h"
#include "IopHw.h"
#include "GS/Renderers/Common/GSFunctionMap.h"
#include "GS.h"
#include "Host.h"
#include "MTVU.h"
#include "SPU2/spu2.h"
#include "SaveState.h"
#include "VUmicro.h"
#include "ps2/HwInternal.h"
#include "ps2/BiosTools.h"
#include "common/AlignedMalloc.h"
#include "common/Error.h"
#include <cstdio>
#include <cstdlib>
#ifdef __linux__
#include <sys/mman.h>
#endif
#ifdef ENABLECACHE
#include "Cache.h"
#endif
#ifdef __APPLE__
#include "common/Darwin/DarwinMisc.h"
#include <TargetConditionals.h>
#endif
namespace Ps2MemSize
{
u32 ExposedRam = MainRam;
u32 ExposedIopRam = IopRam;
} // namespace Ps2MemSize
namespace SysMemory
{
static bool AllocateMemoryMap();
static void DumpMemoryMap();
static void ReleaseMemoryMap();
static u8* s_data_memory;
static void* s_data_memory_file_handle;
static u8* s_code_memory;
static std::unique_ptr<SharedMemoryMappingArea> s_memory_mapping_area;
static std::unique_ptr<SharedMemoryMappingArea> s_code_mapping_area;
} // namespace SysMemory
static void memAllocate();
static void memReset();
static void memRelease();
int MemMode = 0; // 0 is Kernel Mode, 1 is Supervisor Mode, 2 is User Mode
static u16 s_ba[0xff];
static u16 s_dve_regs[0xff];
static bool s_ba_command_executing = false;
static bool s_ba_error_detected = false;
static u16 s_ba_current_reg = 0;
static bool s_extra_memory = false;
namespace HostMemoryMap
{
// For debuggers
extern "C" {
#ifdef _WIN32
_declspec(dllexport) uptr EEmem, IOPmem, VUmem;
#else
__attribute__((visibility("default"), used)) uptr EEmem, IOPmem, VUmem;
#endif
}
} // namespace HostMemoryMap
bool SysMemory::AllocateMemoryMap()
{
s_data_memory_file_handle = HostSys::CreateSharedMemory(HostSys::GetFileMappingName("pcsx2").c_str(), HostMemoryMap::MainSize);
if (!s_data_memory_file_handle)
{
Host::ReportErrorAsync("Error", "Failed to create shared memory file.");
ReleaseMemoryMap();
return false;
}
// Constant-VA placement for the on-disk VU program cache: on arm64 the
// data + code reservations must sit at the same VAs every run so cached
// JIT code reloads without repatching its baked addresses. 4GB clears the
// ASLR brk window (non-PIE image at 0x400000 + brk randomization < 2GB) and
// sits far below the mmap_base / PIE-load regions, so the slot-0 candidate
// succeeds deterministically; Create() walks 256MB-stride fallback slots and
// finally kernel placement (program-cache misses, never corruption). Other
// arches pass 0 and take kernel-chosen placement. The code area is hinted
// directly after the data area, reproducing a contiguous arena when both land.
#if defined(__aarch64__) || defined(_M_ARM64)
constexpr uptr kArenaBase = 0x100000000ull; // 4GB
#else
constexpr uptr kArenaBase = 0;
#endif
Console.WriteLn("@@MAC_MEMMAP@@ data_area_begin size=%zu", static_cast<size_t>(HostMemoryMap::MainSize));
if (!(s_memory_mapping_area = SharedMemoryMappingArea::Create(HostMemoryMap::MainSize, false, kArenaBase)))
{
Host::ReportErrorAsync("Error", "Failed to map main memory.");
ReleaseMemoryMap();
return false;
}
if ((s_data_memory = s_memory_mapping_area->Map(s_data_memory_file_handle, 0, s_memory_mapping_area->BasePointer(), HostMemoryMap::MainSize, PageAccess_ReadWrite())) == nullptr)
{
Host::ReportErrorAsync("Error", "Failed to map data memory.");
ReleaseMemoryMap();
return false;
}
Console.WriteLn("@@MAC_MEMMAP@@ code_area_begin size=%zu", static_cast<size_t>(HostMemoryMap::CodeSize));
#if defined(__APPLE__) && TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
// [iOS] Code is allocated separately via DarwinMisc dual-mapping (W^X RW/RX aliases),
// not through SharedMemoryMappingArea. iOS rejects PROT_NONE MAP_JIT and MAP_FIXED+MAP_JIT,
// so the generic SharedMemoryMappingArea code path is unusable here.
//
// In forced-interpreter mode (JIT unavailable/expired), skip executable code
// allocation entirely. The interpreter does not generate native code, and
// intCpu::Reserve() never touches s_code_memory.
if (!DarwinMisc::iPSX2_FORCE_EE_INTERP)
{
s_code_memory = static_cast<u8*>(DarwinMisc::MmapCodeDualMap(HostMemoryMap::CodeSize));
if (!s_code_memory)
{
DarwinMisc::iPSX2_FORCE_EE_INTERP = 1;
Console.Warning("iOS executable code-memory allocation failed; continuing with interpreter providers");
std::fprintf(stderr, "@@JIT_FALLBACK@@ reason=ios_code_alloc_failed backend=interpreter\n");
std::fflush(stderr);
}
else
{
Console.WriteLn("@@P43_OFFSET@@ g_code_rw_offset=%ld rw_base=%p size=%zu",
(long)DarwinMisc::g_code_rw_offset, reinterpret_cast<void*>(DarwinMisc::g_code_rw_base),
static_cast<size_t>(DarwinMisc::g_code_rw_size));
}
}
else
{
Console.WriteLn("[iOS] Skipping code-memory allocation \u2014 interpreter-only mode (iPSX2_FORCE_EE_INTERP=1)");
s_code_memory = nullptr;
}
#else
#ifdef __APPLE__
// [jit-transplant] CI-only test hook: ARMSX2_FORCE_DUAL_MAP=1 routes macOS
// through the iOS dual-map allocator (vm_remap RW alias, g_code_rw_offset
// != 0) so the recompiler test suite exercises every RW-alias write path
// without an iOS device. Production macOS takes the SharedMemoryMappingArea
// MAP_JIT path below, unchanged.
const bool skip_code_memory = DarwinMisc::iPSX2_FORCE_EE_INTERP;
const char* const force_dual_map = std::getenv("ARMSX2_FORCE_DUAL_MAP");
if (skip_code_memory)
{
Console.WriteLn("[Apple] Skipping code-memory allocation — interpreter-only mode");
s_code_memory = nullptr;
}
else if (force_dual_map && std::atoi(force_dual_map) == 1)
{
if ((s_code_memory = static_cast<u8*>(DarwinMisc::MmapCodeDualMap(HostMemoryMap::CodeSize))) == nullptr)
{
Host::ReportErrorAsync("Error", "Failed to allocate forced dual-map code memory.");
ReleaseMemoryMap();
return false;
}
}
else
#endif
{
if (!(s_code_mapping_area = SharedMemoryMappingArea::Create(HostMemoryMap::CodeSize, true, kArenaBase ? kArenaBase + HostMemoryMap::MainSize : 0)))
{
Host::ReportErrorAsync("Error", "Failed to map code memory.");
ReleaseMemoryMap();
return false;
}
if ((s_code_memory = s_code_mapping_area->Map(nullptr, 0, s_code_mapping_area->BasePointer(), HostMemoryMap::CodeSize, PageAccess_Any())) == nullptr)
{
Host::ReportErrorAsync("Error", "Failed to allocate code memory.");
ReleaseMemoryMap();
return false;
}
}
#endif
#ifdef __linux__
// FX-15 (design credit FEX-Emu): back the hot JIT code caches with
// transparent hugepages to cut iTLB pressure. madvise is what the
// Rocknix default THP mode ("madvise") honors, and the code half is a
// private anonymous mapping, which is what THP backs. Scoped to the
// EE+IOP and mVU0+mVU1 rec caches — each pair contiguous in the map —
// leaving the VIF/SW-renderer tail alone. A/B off-arm: launch under
// prctl(PR_SET_THP_DISABLE) (see tools/perf/fx15_thp_ab.sh) — it
// survives execve, so no in-tree gate is needed.
static_assert(HostMemoryMap::IOPrecOffset == HostMemoryMap::EErecOffset + HostMemoryMap::EErecSize);
static_assert(HostMemoryMap::mVU1recOffset == HostMemoryMap::mVU0recOffset + HostMemoryMap::mVU0recSize);
madvise(s_code_memory + HostMemoryMap::EErecOffset,
HostMemoryMap::EErecSize + HostMemoryMap::IOPrecSize, MADV_HUGEPAGE);
madvise(s_code_memory + HostMemoryMap::mVU0recOffset,
HostMemoryMap::mVU0recSize + HostMemoryMap::mVU1recSize, MADV_HUGEPAGE);
#endif
HostMemoryMap::EEmem = (uptr)(s_data_memory + HostMemoryMap::EEmemOffset);
HostMemoryMap::IOPmem = (uptr)(s_data_memory + HostMemoryMap::IOPmemOffset);
HostMemoryMap::VUmem = (uptr)(s_data_memory + HostMemoryMap::VUmemOffset);
#ifdef __APPLE__
DarwinMisc::SetJitRange(s_code_memory, HostMemoryMap::CodeSize);
Console.WriteLn("@@P43_OFFSET@@ g_code_rw_offset=%ld", (long)DarwinMisc::g_code_rw_offset);
#endif
DumpMemoryMap();
return true;
}
void SysMemory::DumpMemoryMap()
{
#define DUMP_REGION(name, base, offset, size) \
DevCon.WriteLn(Color_Gray, " %-32s @ 0x%016" PRIXPTR " -> 0x%016" PRIXPTR " %s", name, \
(uptr)(base + offset), (uptr)(base + offset + size), fmt::format("[{}mb]", size / _1mb).c_str());
DUMP_REGION("EE Main Memory", s_data_memory, HostMemoryMap::EEmemOffset, HostMemoryMap::EEmemSize);
DUMP_REGION("IOP Main Memory", s_data_memory, HostMemoryMap::IOPmemOffset, HostMemoryMap::IOPmemSize);
DUMP_REGION("VU0/1 On-Chip Memory", s_data_memory, HostMemoryMap::VUmemOffset, HostMemoryMap::VUmemSize);
DUMP_REGION("VTLB Virtual Map", s_data_memory, HostMemoryMap::VTLBVirtualMapOffset, HostMemoryMap::VTLBVirtualMapSize);
DUMP_REGION("VTLB Address Map", s_data_memory, HostMemoryMap::VTLBAddressMapOffset, HostMemoryMap::VTLBAddressMapSize);
if (HasCodeMemory())
{
DUMP_REGION("R5900 Recompiler Cache", s_code_memory, HostMemoryMap::EErecOffset, HostMemoryMap::EErecSize);
DUMP_REGION("R3000A Recompiler Cache", s_code_memory, HostMemoryMap::IOPrecOffset, HostMemoryMap::IOPrecSize);
DUMP_REGION("Micro VU0 Recompiler Cache", s_code_memory, HostMemoryMap::mVU0recOffset, HostMemoryMap::mVU0recSize);
DUMP_REGION("Micro VU1 Recompiler Cache", s_code_memory, HostMemoryMap::mVU1recOffset, HostMemoryMap::mVU1recSize);
DUMP_REGION("VIF0 Unpack Recompiler Cache", s_code_memory, HostMemoryMap::VIF0recOffset, HostMemoryMap::VIF0recSize);
DUMP_REGION("VIF1 Unpack Recompiler Cache", s_code_memory, HostMemoryMap::VIF1recOffset, HostMemoryMap::VIF1recSize);
DUMP_REGION("VIF Unpack Recompiler Cache", s_code_memory, HostMemoryMap::VIFUnpackRecOffset, HostMemoryMap::VIFUnpackRecSize);
DUMP_REGION("GS Software Renderer", s_code_memory, HostMemoryMap::SWrecOffset, HostMemoryMap::SWrecSize);
}
else
{
DevCon.WriteLn(Color_Gray, " Executable code caches unavailable (interpreter-only mode)");
}
#undef DUMP_REGION
}
void SysMemory::ReleaseMemoryMap()
{
if (s_code_memory)
{
#if defined(__APPLE__) && TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
DarwinMisc::MunmapCodeDualMap(s_code_memory, HostMemoryMap::CodeSize);
#else
if (s_code_mapping_area)
s_code_mapping_area->Unmap(s_code_memory, HostMemoryMap::CodeSize, false);
#ifdef __APPLE__
else
// macOS ARMSX2_FORCE_DUAL_MAP test hook allocated via MmapCodeDualMap.
DarwinMisc::MunmapCodeDualMap(s_code_memory, HostMemoryMap::CodeSize);
#endif
#endif
s_code_memory = nullptr;
}
s_code_mapping_area.reset();
#ifdef __APPLE__
DarwinMisc::SetJitRange(nullptr, 0);
#endif
if (s_data_memory)
{
if (s_memory_mapping_area)
s_memory_mapping_area->Unmap(s_data_memory, HostMemoryMap::MainSize, true);
s_data_memory = nullptr;
}
s_memory_mapping_area.reset();
if (s_data_memory_file_handle)
{
HostSys::DestroySharedMemory(s_data_memory_file_handle);
s_data_memory_file_handle = nullptr;
}
}
void SysMemory::ReserveMemory()
{
// Claim the host memory map (and the arm64 constant-VA arena) up front, so
// the fixed-base placement isn't lost to an intervening heap/mmap. Idempotent.
if (!s_data_memory_file_handle)
AllocateMemoryMap();
}
bool SysMemory::Allocate()
{
DevCon.WriteLn(Color_StrongBlue, "Allocating host memory for virtual systems...");
if (!s_data_memory_file_handle && !AllocateMemoryMap())
return false;
memAllocate();
iopMemAlloc();
vuMemAllocate();
if (!vtlb_Core_Alloc())
{
// Clean up already-allocated memory so the destructor doesn't assert
// ("No mappings left") on a failed boot path.
vuMemRelease();
iopMemRelease();
memRelease();
ReleaseMemoryMap();
return false;
}
return true;
}
void SysMemory::Reset()
{
DevCon.WriteLn(Color_StrongBlue, "Resetting host memory for virtual systems...");
memReset();
iopMemReset();
vuMemReset();
// Note: newVif is reset as part of other VIF structures.
// Software is reset on the GS thread.
}
void SysMemory::Release()
{
Console.WriteLn(Color_Blue, "Releasing host memory for virtual systems...");
vtlb_Core_Free(); // Just to be sure... (calling order could result in it getting missed during Decommit).
vuMemRelease();
iopMemRelease();
memRelease();
ReleaseMemoryMap();
}
bool SysMemory::IsAllocated()
{
return s_data_memory != nullptr;
}
bool SysMemory::HasCodeMemory()
{
return s_code_memory != nullptr;
}
u8* SysMemory::GetDataPtr(size_t offset)
{
pxAssert(offset <= HostMemoryMap::MainSize);
return s_data_memory + offset;
}
u8* SysMemory::GetCodePtr(size_t offset)
{
pxAssert(offset <= HostMemoryMap::CodeSize);
return s_code_memory ? (s_code_memory + offset) : nullptr;
}
void* SysMemory::GetDataFileHandle()
{
return s_data_memory_file_handle;
}
bool memGetExtraMemMode()
{
return s_extra_memory;
}
void memSetExtraMemMode(bool mode)
{
#ifdef ARCH_ARM64
// The ARM64 EE recompiler is MainRam-only: its LUT loop, recLutEntries, the
// recRAM advance, the alias mask and the manual_page/manual_counter arrays are
// all sized to Ps2MemSize::MainRam, where the x86 rec sizes the same things to
// ExposedRam. Pages 0x0200-0x1FFF therefore keep the unmapped default, and
// dispatching into one lands on UnmappedRecLUTPage -> recError. Converting all
// of them together is real work and has to happen as one change (c4d0a8a47c
// spells out why); until it does, refuse the setting at the seam rather than
// let a user-selectable option fail as a recError deep inside a game. The
// interpreter handles the 128MB map fine, so gate on the recompiler only.
if (mode && EmuConfig.Cpu.Recompiler.EnableEE)
{
Console.Warning("Extended RAM (128MB) is not supported by the ARM64 EE recompiler; ignoring it. "
"Disable the EE recompiler if you need it.");
mode = false;
}
#endif
s_extra_memory = mode;
// update the amount of RAM exposed to the VM
Ps2MemSize::ExposedRam = mode ? Ps2MemSize::TotalRam : Ps2MemSize::MainRam;
Ps2MemSize::ExposedIopRam = mode ? Ps2MemSize::TotalIopRam: Ps2MemSize::IopRam;
}
void memSetKernelMode() {
//Do something here
MemMode = 0;
}
void memSetSupervisorMode() {
}
void memSetUserMode() {
}
// These regs are related to DEV9 and DVE stuff, we don't have to go crazy with this, but this sucks less than the original code
void ba0W16(u32 mem, u16 value)
{
//MEM_LOG("ba000000 Memory write16 address %x value %x", mem, value);
u32 masked_mem = (mem & 0xFF);
if (masked_mem == 0x6) // Status Reg
{
s_ba[0x6] &= ~3;
}
else
s_ba[masked_mem] = value;
if (masked_mem == 0x00) // Command Execute Reg
{
if (s_ba[0x2] == 0x4F || s_ba[0x2] == 0x41)
{
DevCon.Warning("Error running DVE command, Control Reg value set to %x", value);
s_ba_error_detected = true;
}
else if (s_ba[masked_mem] & 0x80) // Start executing
{
if (s_ba[0x2] == 0x43) // Write Mode
{
int size = (s_ba[masked_mem] & 0xF);
s_ba_current_reg = s_ba[0x10];
size--;
// 0x10->0x22 seems to be some sort of FIFO, with 0x10 generally being the register to read/write
for (int i = 0; i < size; i++)
{
s_dve_regs[s_ba_current_reg] = s_ba[0x12 + i];
}
s_ba_command_executing = true;
s_ba_error_detected = false;
}
else if (s_ba[0x2] == 0x42) // Read Mode
{
int size = (s_ba[masked_mem] & 0xF);
for (int i = 0; i < size; i++)
s_ba[0x10 + i] = s_dve_regs[s_ba_current_reg]; // Probably not right but we don't access the real regs, will be enough for now.
s_ba_command_executing = true;
s_ba_error_detected = false;
}
}
}
else if (masked_mem == 0xA) // Power/Standby (?) Reg
{
if (value == 0)
s_ba_error_detected = true;
else
s_ba_error_detected = false;
DevCon.Warning("DVE powered %s", value == 0 ? "off" : "on");
}
}
u16 ba0R16(u32 mem)
{
//MEM_LOG("ba000000 Memory read16 address %x", mem);
if (mem == 0x1a000006)
{
// 0xba00000A bit 0 is kind of an "on" switch. bit 0 of ba000006 seems to be the powered off/error bit.
// bit 1 in ba000006 seems to be "ready".
u16 return_val = (s_ba[0x6] & 2);
if (s_ba_error_detected)
return_val |= 1;
if (s_ba[0x6] < 3 && s_ba_command_executing)
s_ba[0x6]++;
else
s_ba_command_executing = false;
return return_val;
}
return s_ba[mem & 0x1F];
}
#define CHECK_MEM(mem) //MyMemCheck(mem)
void MyMemCheck(u32 mem)
{
if ( mem == 0x1c02f2a0 )
Console.WriteLn("yo; (mem == 0x1c02f2a0) in MyMemCheck...");
}
/////////////////////////////
// REGULAR MEM START
/////////////////////////////
static vtlbHandler
null_handler,
tlb_fallback_0,
tlb_fallback_2,
tlb_fallback_3,
tlb_fallback_4,
tlb_fallback_5,
tlb_fallback_6,
tlb_fallback_7,
tlb_fallback_8,
vu0_micro_mem,
vu1_micro_mem,
vu1_data_mem,
hw_by_page[0x10] = { 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF},
gs_page_0,
gs_page_1,
iopHw_by_page_01,
iopHw_by_page_03,
iopHw_by_page_08,
iop_memory;
void memMapVUmicro()
{
// VU0/VU1 micro mem (instructions)
// (Like IOP memory, these are generally only used by the EE Bios kernel during
// boot-up. Applications/games are "supposed" to use the thread-safe VIF instead;
// or must ensure all VIF/GIF transfers are finished and all VUmicro execution stopped
// prior to accessing VU memory directly).
// The VU0 mapping actually repeats 4 times across the mapped range, but we don't bother
// to manually mirror it here because the indirect memory handler for it (see vuMicroRead*
// functions below) automatically mask and wrap the address for us.
vtlb_MapHandler(vu0_micro_mem,0x11000000,0x00004000);
vtlb_MapHandler(vu1_micro_mem,0x11008000,0x00004000);
// VU0/VU1 memory (data)
// VU0 is 4k, mirrored 4 times across a 16k area.
vtlb_MapBlock(VU0.Mem,0x11004000,0x00004000,0x1000);
// Note: In order for the below conditional to work correctly
// support needs to be coded to reset the memMappings when MTVU is
// turned off/on. For now we just always use the vu data handlers...
if (1||THREAD_VU1) vtlb_MapHandler(vu1_data_mem,0x1100c000,0x00004000);
else vtlb_MapBlock (VU1.Mem, 0x1100c000,0x00004000);
}
void memMapPhy()
{
// Main memory
vtlb_MapBlock(eeMem->Main, 0x00000000,Ps2MemSize::ExposedRam);//mirrored on first 256 mb ?
// High memory, uninstalled on the configuration we emulate
vtlb_MapHandler(null_handler, Ps2MemSize::ExposedRam, 0x10000000 - Ps2MemSize::ExposedRam);
// Physical RAM mirrors used by BIOS InitRDRAM for RDRAM device configuration.
// On real PS2 hardware:
// 0x20000000-0x21FFFFFF = uncached mirror of main RAM
// 0x30000000-0x31FFFFFF = uncached & accelerated mirror of main RAM
// These mirrors must be present in the physical map; without them, BIOS writes
// to RDRAM device registers hit UnmappedPhyHandler (bus error).
// Requires VTLB_PMAP_SZ >= 1GB to cover these addresses.
vtlb_MapBlock(eeMem->Main, 0x20000000, Ps2MemSize::ExposedRam);
vtlb_MapBlock(eeMem->Main, 0x30000000, Ps2MemSize::ExposedRam);
// Various ROMs (all read-only)
vtlb_MapBlock(eeMem->ROM, 0x1fc00000, Ps2MemSize::Rom);
vtlb_MapBlock(eeMem->ROM1, 0x1e000000, Ps2MemSize::Rom1);
vtlb_MapBlock(eeMem->ROM2, 0x1e400000, Ps2MemSize::Rom2);
// IOP memory
// (used by the EE Bios Kernel during initial hardware initialization, Apps/Games
// are "supposed" to use the thread-safe SIF instead.)
vtlb_MapHandler(iop_memory,0x1c000000,0x00800000);
// Generic Handlers; These fallback to mem* stuff...
vtlb_MapHandler(tlb_fallback_7,0x14000000, _64kb);
vtlb_MapHandler(tlb_fallback_4,0x18000000, _64kb);
vtlb_MapHandler(tlb_fallback_5,0x1a000000, _64kb);
vtlb_MapHandler(tlb_fallback_6,0x12000000, _64kb);
vtlb_MapHandler(tlb_fallback_8,0x1f000000, _64kb);
vtlb_MapHandler(tlb_fallback_3,0x1f400000, _64kb);
vtlb_MapHandler(tlb_fallback_2,0x1f800000, _64kb);
vtlb_MapHandler(tlb_fallback_8,0x1f900000, _64kb);
// Hardware Register Handlers : specialized/optimized per-page handling of HW register accesses
// (note that hw_by_page handles are assigned in memReset prior to calling this function)
for( uint i=0; i<16; ++i)
vtlb_MapHandler(hw_by_page[i], 0x10000000 + (0x01000 * i), 0x01000);
vtlb_MapHandler(gs_page_0, 0x12000000, 0x01000);
vtlb_MapHandler(gs_page_1, 0x12001000, 0x01000);
// "Secret" IOP HW mappings - Used by EE Bios Kernel during boot and generally
// left untouched after that, as per EE/IOP thread safety rules.
vtlb_MapHandler(iopHw_by_page_01, 0x1f801000, 0x01000);
vtlb_MapHandler(iopHw_by_page_03, 0x1f803000, 0x01000);
vtlb_MapHandler(iopHw_by_page_08, 0x1f808000, 0x01000);
}
//Why is this required ?
void memMapKernelMem()
{
//lower 512 mb: direct map
//vtlb_VMap(0x00000000,0x00000000,0x20000000);
//0x8* mirror
vtlb_VMap(0x80000000, 0x00000000, _1mb*512);
//0xa* mirror
vtlb_VMap(0xA0000000, 0x00000000, _1mb*512);
}
//what do do with these ?
void memMapSupervisorMem()
{
}
void memMapUserMem()
{
}
static mem8_t nullRead8(u32 mem) {
MEM_LOG("Read uninstalled memory at address %08x", mem);
return 0;
}
static mem16_t nullRead16(u32 mem) {
MEM_LOG("Read uninstalled memory at address %08x", mem);
return 0;
}
static mem32_t nullRead32(u32 mem) {
MEM_LOG("Read uninstalled memory at address %08x", mem);
return 0;
}
static mem64_t nullRead64(u32 mem) {
MEM_LOG("Read uninstalled memory at address %08x", mem);
return 0;
}
static RETURNS_R128 nullRead128(u32 mem) {
MEM_LOG("Read uninstalled memory at address %08x", mem);
return r128_zero();
}
static void nullWrite8(u32 mem, mem8_t value)
{
MEM_LOG("Write uninstalled memory at address %08x", mem);
}
static void nullWrite16(u32 mem, mem16_t value)
{
MEM_LOG("Write uninstalled memory at address %08x", mem);
}
static void nullWrite32(u32 mem, mem32_t value)
{
MEM_LOG("Write uninstalled memory at address %08x", mem);
}
static void nullWrite64(u32 mem, mem64_t value)
{
MEM_LOG("Write uninstalled memory at address %08x", mem);
}
static void TAKES_R128 nullWrite128(u32 mem, r128 value)
{
MEM_LOG("Write uninstalled memory at address %08x", mem);
}
template<int p>
static mem8_t _ext_memRead8 (u32 mem)
{
switch (p)
{
case 3: // psh4
return psxHw4Read8(mem);
case 6: // gsm
return gsRead8(mem);
case 7: // dev9
{
mem8_t retval = DEV9read8(mem & ~0xa4000000);
Console.WriteLn("DEV9 read8 %8.8lx: %2.2lx", mem & ~0xa4000000, retval);
return retval;
}
case 9:
return iopMemRead8(mem & ~0x1c000000);
default: break;
}
MEM_LOG("Unknown Memory Read8 from address %8.8x", mem);
cpuTlbMissR(mem, cpuRegs.branch);
return 0;
}
template<int p>
static mem16_t _ext_memRead16(u32 mem)
{
switch (p)
{
case 4: // b80
MEM_LOG("b800000 Memory read16 address %x", mem);
return 0;
case 5: // ba0
MEM_LOG("ba000000 Memory read16 address %x", mem);
return ba0R16(mem);
case 6: // gsm
return gsRead16(mem);
case 7: // dev9
{
mem16_t retval = DEV9read16(mem & ~0xa4000000);
Console.WriteLn("DEV9 read16 %8.8lx: %4.4lx", mem & ~0xa4000000, retval);
return retval;
}
case 8: // spu2
return SPU2read(mem);
case 9:
return iopMemRead16(mem & ~0x1c000000);
default: break;
}
MEM_LOG("Unknown Memory read16 from address %8.8x", mem);
cpuTlbMissR(mem, cpuRegs.branch);
return 0;
}
template<int p>
static mem32_t _ext_memRead32(u32 mem)
{
switch (p)
{
case 6: // gsm
return gsRead32(mem);
case 7: // dev9
{
mem32_t retval = DEV9read32(mem & ~0xa4000000);
Console.WriteLn("DEV9 read32 %8.8lx: %8.8lx", mem & ~0xa4000000, retval);
return retval;
}
case 9:
return iopMemRead32(mem & ~0x1c000000);
default: break;
}
MEM_LOG("Unknown Memory read32 from address %8.8x (Status=%8.8x)", mem, cpuRegs.CP0.n.Status.val);
cpuTlbMissR(mem, cpuRegs.branch);
return 0;
}
template<int p>
static u64 _ext_memRead64(u32 mem)
{
switch (p)
{
case 6: // gsm
return gsRead64(mem);
case 9:
{
u64 ret = 0;
ret |= ((u64)(iopMemRead32((mem + 0) & ~0x1c000000)) << 0);
ret |= ((u64)(iopMemRead32((mem + 4) & ~0x1c000000)) << 32);
return ret;
}
default: break;
}
MEM_LOG("Unknown Memory read64 from address %8.8x", mem);
cpuTlbMissR(mem, cpuRegs.branch);
return 0;
}
template<int p>
static RETURNS_R128 _ext_memRead128(u32 mem)
{
switch (p)
{
//case 1: // hwm
// return hwRead128(mem & ~0xa0000000);
case 6: // gsm
return r128_load(PS2GS_BASE(mem));
case 9:
{
u128 ret = {};
ret._u32[0] = iopMemRead32((mem + 0) & ~0x1c000000);
ret._u32[1] = iopMemRead32((mem + 4) & ~0x1c000000);
ret._u32[2] = iopMemRead32((mem + 8) & ~0x1c000000);
ret._u32[3] = iopMemRead32((mem + 12) & ~0x1c000000);
return r128_from_u128(ret);
}
default: break;
}
MEM_LOG("Unknown Memory read128 from address %8.8x", mem);
cpuTlbMissR(mem, cpuRegs.branch);
return r128_zero();
}
template<int p>
static void _ext_memWrite8 (u32 mem, mem8_t value)
{
switch (p) {
case 3: // psh4
psxHw4Write8(mem, value); return;
case 6: // gsm
gsWrite8(mem, value); return;
case 7: // dev9
DEV9write8(mem & ~0xa4000000, value);
Console.WriteLn("DEV9 write8 %8.8lx: %2.2lx", mem & ~0xa4000000, value);
return;
case 9:
iopMemWrite8(mem & ~0x1c000000, value);
return;
default: break;
}
MEM_LOG("Unknown Memory write8 to address %x with data %2.2x", mem, value);
cpuTlbMissW(mem, cpuRegs.branch);
}
template<int p>
static void _ext_memWrite16(u32 mem, mem16_t value)
{
switch (p) {
case 5: // ba0
MEM_LOG("ba000000 Memory write16 address %x value %x", mem, value);
ba0W16(mem, value);
return;
case 6: // gsm
gsWrite16(mem, value); return;
case 7: // dev9
DEV9write16(mem & ~0xa4000000, value);
Console.WriteLn("DEV9 write16 %8.8lx: %4.4lx", mem & ~0xa4000000, value);
return;
case 8: // spu2
SPU2write(mem, value); return;
case 9:
iopMemWrite16(mem & ~0x1c000000, value);
return;
default: break;
}
MEM_LOG("Unknown Memory write16 to address %x with data %4.4x", mem, value);
cpuTlbMissW(mem, cpuRegs.branch);
}
template<int p>
static void _ext_memWrite32(u32 mem, mem32_t value)
{
switch (p) {
case 6: // gsm
gsWrite32(mem, value); return;
case 7: // dev9
DEV9write32(mem & ~0xa4000000, value);
Console.WriteLn("DEV9 write32 %8.8lx: %8.8lx", mem & ~0xa4000000, value);
return;
case 9:
iopMemWrite32(mem & ~0x1c000000, value);
return;
default: break;
}
MEM_LOG("Unknown Memory write32 to address %x with data %8.8x", mem, value);
cpuTlbMissW(mem, cpuRegs.branch);
}
template<int p>
static void _ext_memWrite64(u32 mem, mem64_t value)
{
switch (p)
{
case 9:
iopMemWrite32((mem + 0) & ~0x1c000000, (value >> 0) & 0xffffffff);
iopMemWrite32((mem + 4) & ~0x1c000000, (value >> 32) & 0xffffffff);
return;
default:
break;
}
/*switch (p) {
//case 1: // hwm
// hwWrite64(mem & ~0xa0000000, *value);
// return;
//case 6: // gsm
// gsWrite64(mem & ~0xa0000000, *value); return;
}*/
MEM_LOG("Unknown Memory write64 to address %x with data %8.8x_%8.8x", mem, (u32)(value>>32), (u32)value);
cpuTlbMissW(mem, cpuRegs.branch);
}
template<int p>
static void TAKES_R128 _ext_memWrite128(u32 mem, r128 value)
{
switch (p)
{
case 9:
{
u128 val = r128_to_u128(value);
iopMemWrite32((mem + 0) & ~0x1c000000, val._u32[0] & 0xffffffff);
iopMemWrite32((mem + 4) & ~0x1c000000, val._u32[1] & 0xffffffff);
iopMemWrite32((mem + 8) & ~0x1c000000, val._u32[2] & 0xffffffff);
iopMemWrite32((mem + 12) & ~0x1c000000, val._u32[3] & 0xffffffff);
return;
}
default:
break;
}
/*switch (p) {
//case 1: // hwm
// hwWrite128(mem & ~0xa0000000, value);
// return;
//case 6: // gsm
// mem &= ~0xa0000000;
// gsWrite64(mem, value[0]);
// gsWrite64(mem+8, value[1]); return;
}*/
alignas(16) const u128 uvalue = r128_to_u128(value);
MEM_LOG("Unknown Memory write128 to address %x with data %8.8x_%8.8x_%8.8x_%8.8x", mem, uvalue._u32[3], uvalue._u32[2], uvalue._u32[1], uvalue._u32[0]);
cpuTlbMissW(mem, cpuRegs.branch);
}
#define vtlb_RegisterHandlerTempl1(nam,t) vtlb_RegisterHandler(nam##Read8<t>,nam##Read16<t>,nam##Read32<t>,nam##Read64<t>,nam##Read128<t>, \
nam##Write8<t>,nam##Write16<t>,nam##Write32<t>,nam##Write64<t>,nam##Write128<t>)
typedef void ClearFunc_t( u32 addr, u32 qwc );
template<int vunum> static __fi void ClearVuFunc(u32 addr, u32 size) {
if (vunum) CpuVU1->Clear(addr, size);
else CpuVU0->Clear(addr, size);
}
// VU Micro Memory Reads...
template<int vunum> static mem8_t vuMicroRead8(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return vu->Micro[addr];
}
template<int vunum> static mem16_t vuMicroRead16(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return *(u16*)&vu->Micro[addr];
}
template<int vunum> static mem32_t vuMicroRead32(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return *(u32*)&vu->Micro[addr];
}
template<int vunum> static mem64_t vuMicroRead64(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return *(u64*)&vu->Micro[addr];
}
template<int vunum> static RETURNS_R128 vuMicroRead128(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return r128_load(&vu->Micro[addr]);
}
// Profiled VU writes: Happen very infrequently, with exception of BIOS initialization (at most twice per
// frame in-game, and usually none at all after BIOS), so cpu clears aren't much of a big deal.
template<int vunum> static void vuMicroWrite8(u32 addr,mem8_t data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) {
vu1Thread.WriteMicroMem(addr, &data, sizeof(u8));
return;
}
if (vu->Micro[addr]!=data) { // Clear before writing new data
ClearVuFunc<vunum>(addr, 8); //(clearing 8 bytes because an instruction is 8 bytes) (cottonvibes)
vu->Micro[addr] =data;
}
}
template<int vunum> static void vuMicroWrite16(u32 addr, mem16_t data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) {
vu1Thread.WriteMicroMem(addr, &data, sizeof(u16));
return;
}
if (*(u16*)&vu->Micro[addr]!=data) {
ClearVuFunc<vunum>(addr, 8);
*(u16*)&vu->Micro[addr] =data;
}
}
template<int vunum> static void vuMicroWrite32(u32 addr, mem32_t data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) {
vu1Thread.WriteMicroMem(addr, &data, sizeof(u32));
return;
}
if (*(u32*)&vu->Micro[addr]!=data) {
ClearVuFunc<vunum>(addr, 8);
*(u32*)&vu->Micro[addr] =data;
}
}
template<int vunum> static void vuMicroWrite64(u32 addr, mem64_t data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) {
vu1Thread.WriteMicroMem(addr, &data, sizeof(u64));
return;
}
if (*(u64*)&vu->Micro[addr]!=data) {
ClearVuFunc<vunum>(addr, 8);
*(u64*)&vu->Micro[addr] =data;
}
}
template<int vunum> static void TAKES_R128 vuMicroWrite128(u32 addr, r128 data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
const u128 udata = r128_to_u128(data);
if (vunum && THREAD_VU1) {
vu1Thread.WriteMicroMem(addr, &udata, sizeof(u128));
return;
}
if ((u128&)vu->Micro[addr]!=udata) {
ClearVuFunc<vunum>(addr, 16);
r128_store_unaligned(&vu->Micro[addr],data);
}
}
// VU Data Memory Reads...
template<int vunum> static mem8_t vuDataRead8(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return vu->Mem[addr];
}
template<int vunum> static mem16_t vuDataRead16(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return *(u16*)&vu->Mem[addr];
}
template<int vunum> static mem32_t vuDataRead32(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return *(u32*)&vu->Mem[addr];
}
template<int vunum> static mem64_t vuDataRead64(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return *(u64*)&vu->Mem[addr];
}
template<int vunum> static RETURNS_R128 vuDataRead128(u32 addr) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) vu1Thread.WaitVU();
return r128_load(&vu->Mem[addr]);
}
// VU Data Memory Writes...
template<int vunum> static void vuDataWrite8(u32 addr, mem8_t data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) {
vu1Thread.WriteDataMem(addr, &data, sizeof(u8));
return;
}
vu->Mem[addr] = data;
}
template<int vunum> static void vuDataWrite16(u32 addr, mem16_t data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) {
vu1Thread.WriteDataMem(addr, &data, sizeof(u16));
return;
}
*(u16*)&vu->Mem[addr] = data;
}
template<int vunum> static void vuDataWrite32(u32 addr, mem32_t data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) {
vu1Thread.WriteDataMem(addr, &data, sizeof(u32));
return;
}
*(u32*)&vu->Mem[addr] = data;
}
template<int vunum> static void vuDataWrite64(u32 addr, mem64_t data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) {
vu1Thread.WriteDataMem(addr, &data, sizeof(u64));
return;
}
*(u64*)&vu->Mem[addr] = data;
}
template<int vunum> static void TAKES_R128 vuDataWrite128(u32 addr, r128 data) {
VURegs* vu = vunum ? &VU1 : &VU0;
addr &= vunum ? 0x3fff: 0xfff;
if (vunum && THREAD_VU1) {
alignas(16) const u128 udata = r128_to_u128(data);
vu1Thread.WriteDataMem(addr, &udata, sizeof(u128));
return;
}
r128_store_unaligned(&vu->Mem[addr], data);
}
void memSetPageAddr(u32 vaddr, u32 paddr)
{
//Console.WriteLn("memSetPageAddr: %8.8x -> %8.8x", vaddr, paddr);
vtlb_VMap(vaddr,paddr,0x1000);
}
void memClearPageAddr(u32 vaddr)
{
//Console.WriteLn("memClearPageAddr: %8.8x", vaddr);
vtlb_VMapUnmap(vaddr,0x1000); // -> whut ?
#ifdef FULLTLB
// memLUTRK[vaddr >> 12] = 0;
// memLUTWK[vaddr >> 12] = 0;
#endif
}
///////////////////////////////////////////////////////////////////////////
// PS2 Memory Init / Reset / Shutdown
EEVM_MemoryAllocMess* eeMem = nullptr;
alignas(__pagealignsize) u8 eeHw[Ps2MemSize::Hardware];
void memBindConditionalHandlers()
{
if( hw_by_page[0xf] == 0xFFFFFFFF ) return;
if (EmuConfig.Speedhacks.IntcStat)
{
vtlbMemR16FP* page0F16(hwRead16_page_0F_INTC_HACK);
vtlbMemR32FP* page0F32(hwRead32_page_0F_INTC_HACK);
//vtlbMemR64FP* page0F64(hwRead64_generic_INTC_HACK);
vtlb_ReassignHandler( hw_by_page[0xf],
hwRead8<0x0f>, page0F16, page0F32, hwRead64<0x0f>, hwRead128<0x0f>,
hwWrite8<0x0f>, hwWrite16<0x0f>, hwWrite32<0x0f>, hwWrite64<0x0f>, hwWrite128<0x0f>
);
}
else
{
vtlbMemR16FP* page0F16(hwRead16<0x0f>);
vtlbMemR32FP* page0F32(hwRead32<0x0f>);
//vtlbMemR64FP* page0F64(hwRead64<0x0f>);
vtlb_ReassignHandler( hw_by_page[0xf],
hwRead8<0x0f>, page0F16, page0F32, hwRead64<0x0f>, hwRead128<0x0f>,
hwWrite8<0x0f>, hwWrite16<0x0f>, hwWrite32<0x0f>, hwWrite64<0x0f>, hwWrite128<0x0f>
);
}
}
// --------------------------------------------------------------------------------------
// eeMemoryReserve (implementations)
// --------------------------------------------------------------------------------------
void memAllocate()
{
eeMem = reinterpret_cast<EEVM_MemoryAllocMess*>(SysMemory::GetEEMem());
}
void memReset()
{
// Note!! Ideally the vtlb should only be initialized once, and then subsequent
// resets of the system hardware would only clear vtlb mappings, but since the
// rest of the emu is not really set up to support a "soft" reset of that sort
// we opt for the hard/safe version.
pxAssume( eeMem );
#ifdef ENABLECACHE
memset(pCache,0,sizeof(_cacheS)*64);
#endif
vtlb_Init();
null_handler = vtlb_RegisterHandler(nullRead8, nullRead16, nullRead32, nullRead64, nullRead128,
nullWrite8, nullWrite16, nullWrite32, nullWrite64, nullWrite128);
tlb_fallback_0 = vtlb_RegisterHandlerTempl1(_ext_mem,0);
tlb_fallback_3 = vtlb_RegisterHandlerTempl1(_ext_mem,3);
tlb_fallback_4 = vtlb_RegisterHandlerTempl1(_ext_mem,4);
tlb_fallback_5 = vtlb_RegisterHandlerTempl1(_ext_mem,5);
tlb_fallback_7 = vtlb_RegisterHandlerTempl1(_ext_mem,7);
tlb_fallback_8 = vtlb_RegisterHandlerTempl1(_ext_mem,8);
iop_memory = vtlb_RegisterHandlerTempl1(_ext_mem,9);
// Dynarec versions of VUs
vu0_micro_mem = vtlb_RegisterHandlerTempl1(vuMicro,0);
vu1_micro_mem = vtlb_RegisterHandlerTempl1(vuMicro,1);
vu1_data_mem = (1||THREAD_VU1) ? vtlb_RegisterHandlerTempl1(vuData,1) : 0;
//////////////////////////////////////////////////////////////////////////////////////////
// IOP's "secret" Hardware Register mapping, accessible from the EE (and meant for use
// by debugging or BIOS only). The IOP's hw regs are divided into three main pages in
// the 0x1f80 segment, and then another oddball page for CDVD in the 0x1f40 segment.
//
using namespace IopMemory;
tlb_fallback_2 = vtlb_RegisterHandler(
iopHwRead8_generic, iopHwRead16_generic, iopHwRead32_generic, _ext_memRead64<2>, _ext_memRead128<2>,
iopHwWrite8_generic, iopHwWrite16_generic, iopHwWrite32_generic, _ext_memWrite64<2>, _ext_memWrite128<2>
);
iopHw_by_page_01 = vtlb_RegisterHandler(
iopHwRead8_Page1, iopHwRead16_Page1, iopHwRead32_Page1, _ext_memRead64<2>, _ext_memRead128<2>,
iopHwWrite8_Page1, iopHwWrite16_Page1, iopHwWrite32_Page1, _ext_memWrite64<2>, _ext_memWrite128<2>
);
iopHw_by_page_03 = vtlb_RegisterHandler(
iopHwRead8_Page3, iopHwRead16_Page3, iopHwRead32_Page3, _ext_memRead64<2>, _ext_memRead128<2>,
iopHwWrite8_Page3, iopHwWrite16_Page3, iopHwWrite32_Page3, _ext_memWrite64<2>, _ext_memWrite128<2>
);
iopHw_by_page_08 = vtlb_RegisterHandler(
iopHwRead8_Page8, iopHwRead16_Page8, iopHwRead32_Page8, _ext_memRead64<2>, _ext_memRead128<2>,
iopHwWrite8_Page8, iopHwWrite16_Page8, iopHwWrite32_Page8, _ext_memWrite64<2>, _ext_memWrite128<2>
);
// psHw Optimized Mappings
// The HW Registers have been split into pages to improve optimization.
#define hwHandlerTmpl(page) \
hwRead8<page>, hwRead16<page>, hwRead32<page>, hwRead64<page>, hwRead128<page>, \
hwWrite8<page>, hwWrite16<page>,hwWrite32<page>,hwWrite64<page>,hwWrite128<page>
hw_by_page[0x0] = vtlb_RegisterHandler( hwHandlerTmpl(0x00) );
hw_by_page[0x1] = vtlb_RegisterHandler( hwHandlerTmpl(0x01) );
hw_by_page[0x2] = vtlb_RegisterHandler( hwHandlerTmpl(0x02) );
hw_by_page[0x3] = vtlb_RegisterHandler( hwHandlerTmpl(0x03) );
hw_by_page[0x4] = vtlb_RegisterHandler( hwHandlerTmpl(0x04) );
hw_by_page[0x5] = vtlb_RegisterHandler( hwHandlerTmpl(0x05) );
hw_by_page[0x6] = vtlb_RegisterHandler( hwHandlerTmpl(0x06) );
hw_by_page[0x7] = vtlb_RegisterHandler( hwHandlerTmpl(0x07) );
hw_by_page[0x8] = vtlb_RegisterHandler( hwHandlerTmpl(0x08) );
hw_by_page[0x9] = vtlb_RegisterHandler( hwHandlerTmpl(0x09) );
hw_by_page[0xa] = vtlb_RegisterHandler( hwHandlerTmpl(0x0a) );
hw_by_page[0xb] = vtlb_RegisterHandler( hwHandlerTmpl(0x0b) );
hw_by_page[0xc] = vtlb_RegisterHandler( hwHandlerTmpl(0x0c) );
hw_by_page[0xd] = vtlb_RegisterHandler( hwHandlerTmpl(0x0d) );
hw_by_page[0xe] = vtlb_RegisterHandler( hwHandlerTmpl(0x0e) );
hw_by_page[0xf] = vtlb_NewHandler(); // redefined later based on speedhacking prefs
memBindConditionalHandlers();
//////////////////////////////////////////////////////////////////////
// GS Optimized Mappings
tlb_fallback_6 = vtlb_RegisterHandler(
_ext_memRead8<6>, _ext_memRead16<6>, _ext_memRead32<6>, _ext_memRead64<6>, _ext_memRead128<6>,
_ext_memWrite8<6>, _ext_memWrite16<6>, _ext_memWrite32<6>, gsWrite64_generic, gsWrite128_generic
);
gs_page_0 = vtlb_RegisterHandler(
_ext_memRead8<6>, _ext_memRead16<6>, _ext_memRead32<6>, _ext_memRead64<6>, _ext_memRead128<6>,
_ext_memWrite8<6>, _ext_memWrite16<6>, _ext_memWrite32<6>, gsWrite64_page_00, gsWrite128_page_00
);
gs_page_1 = vtlb_RegisterHandler(
_ext_memRead8<6>, _ext_memRead16<6>, _ext_memRead32<6>, _ext_memRead64<6>, _ext_memRead128<6>,
_ext_memWrite8<6>, _ext_memWrite16<6>, _ext_memWrite32<6>, gsWrite64_page_01, gsWrite128_page_01
);
//vtlb_Reset();
// reset memLUT (?)
//vtlb_VMap(0x00000000,0x00000000,0x20000000);
//vtlb_VMapUnmap(0x20000000,0x60000000);
memMapPhy();
memMapVUmicro();
memMapKernelMem();
memMapSupervisorMem();
memMapUserMem();
memSetKernelMode();
vtlb_VMap(0x00000000,0x00000000,0x20000000);
vtlb_VMapUnmap(0x20000000,0x60000000);
std::memset(s_ba, 0, sizeof(s_ba));
s_ba[0xA] = 1; // Power on
s_ba_command_executing = false;
s_ba_error_detected = false;
s_ba_current_reg = 0;
std::memset(s_dve_regs, 0, sizeof(s_dve_regs));
s_dve_regs[0x7e] = 0x1C; // Status register. 0x1C seems to be the value it's expecting for everything being OK.
// BIOS is included in eeMem, so it needs to be copied after zeroing.
std::memset(eeMem, 0, sizeof(*eeMem));
CopyBIOSToMemory();
}
void memRelease()
{
eeMem = nullptr;
}
bool SaveStateBase::memFreeze(Error* error)
{
Freeze(s_ba);
Freeze(s_dve_regs);
Freeze(s_ba_command_executing);
Freeze(s_ba_error_detected);
Freeze(s_ba_current_reg);
bool extra_memory = s_extra_memory;
Freeze(extra_memory);
if (extra_memory != s_extra_memory)
{
Error::SetStringFmt(error, "Memory size mismatch, save state requires {}, but VM currently has {}.",
extra_memory ? "128MB" : "32MB", s_extra_memory ? "128MB" : "32MB");
return false;
}
return IsOkay();
}
u8 EEMemoryInterface::Read8(u32 address, bool* valid)
{
if (valid)
*valid = true;
return memRead8(address);
}
u16 EEMemoryInterface::Read16(u32 address, bool* valid)
{
if (valid)
*valid = true;
return memRead16(address);
}
u32 EEMemoryInterface::Read32(u32 address, bool* valid)
{
if (valid)
*valid = true;
return memRead32(address);
}
u64 EEMemoryInterface::Read64(u32 address, bool* valid)
{
if (valid)
*valid = true;
return memRead64(address);
}
u128 EEMemoryInterface::Read128(u32 address, bool* valid)
{
u128 value;
memRead128(address, value);
if (valid)
*valid = true;
return value;
}
bool EEMemoryInterface::ReadBytes(u32 address, void* dest, u32 size)
{
return vtlb_memSafeReadBytes(address, dest, size);
}
bool EEMemoryInterface::Write8(u32 address, u8 value)
{
memWrite8(address, value);
return true;
}
bool EEMemoryInterface::Write16(u32 address, u16 value)
{
memWrite16(address, value);
return true;
}
bool EEMemoryInterface::Write32(u32 address, u32 value)
{
memWrite32(address, value);
return true;
}
bool EEMemoryInterface::Write64(u32 address, u64 value)
{
memWrite64(address, value);
return true;
}
bool EEMemoryInterface::Write128(u32 address, u128 value)
{
memWrite128(address, value);
return true;
}
bool EEMemoryInterface::WriteBytes(u32 address, const void* src, u32 size)
{
return vtlb_memSafeWriteBytes(address, src, size);
}
bool EEMemoryInterface::CompareBytes(u32 address, const void* src, u32 size)
{
return vtlb_memSafeCmpBytes(address, src, size) == 0;
}