mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
# 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.
531 lines
16 KiB
C++
531 lines
16 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "Common.h"
|
|
#include "Vif.h"
|
|
#include "Vif_Dma.h"
|
|
#include "Vif_Dynarec.h"
|
|
#include "MTVU.h"
|
|
|
|
enum UnpackOffset {
|
|
OFFSET_X = 0,
|
|
OFFSET_Y = 1,
|
|
OFFSET_Z = 2,
|
|
OFFSET_W = 3
|
|
};
|
|
|
|
static __fi u32 setVifRow(vifStruct& vif, u32 reg, u32 data) {
|
|
vif.MaskRow._u32[reg] = data;
|
|
return data;
|
|
}
|
|
|
|
// cycle derives from vif.cl
|
|
// mode derives from vifRegs.mode
|
|
template< uint idx, uint mode, bool doMask >
|
|
static __ri void writeXYZW(u32 offnum, u32 &dest, u32 data) {
|
|
int n = 0;
|
|
|
|
vifStruct& vif = MTVU_VifX;
|
|
|
|
if (doMask) {
|
|
const VIFregisters& regs = MTVU_VifXRegs;
|
|
switch (vif.cl) {
|
|
case 0: n = (regs.mask >> (offnum * 2)) & 0x3; break;
|
|
case 1: n = (regs.mask >> ( 8 + (offnum * 2))) & 0x3; break;
|
|
case 2: n = (regs.mask >> (16 + (offnum * 2))) & 0x3; break;
|
|
default: n = (regs.mask >> (24 + (offnum * 2))) & 0x3; break;
|
|
}
|
|
}
|
|
|
|
// Four possible types of masking are handled below:
|
|
// 0 - Data
|
|
// 1 - MaskRow
|
|
// 2 - MaskCol
|
|
// 3 - Write protect
|
|
|
|
switch (n) {
|
|
case 0:
|
|
switch (mode) {
|
|
case 1: dest = data + vif.MaskRow._u32[offnum]; break;
|
|
case 2: dest = setVifRow(vif, offnum, vif.MaskRow._u32[offnum] + data); break;
|
|
case 3: dest = setVifRow(vif, offnum, data); break;
|
|
default: dest = data; break;
|
|
}
|
|
break;
|
|
case 1: dest = vif.MaskRow._u32[offnum]; break;
|
|
case 2: dest = vif.MaskCol._u32[std::min(vif.cl,3)]; break;
|
|
case 3: break;
|
|
}
|
|
}
|
|
#define tParam idx,mode,doMask
|
|
|
|
template < uint idx, uint mode, bool doMask, class T >
|
|
static void UNPACK_S(u32* dest, const T* src)
|
|
{
|
|
u32 data = *src;
|
|
|
|
//S-# will always be a complete packet, no matter what. So we can skip the offset bits
|
|
writeXYZW<tParam>(OFFSET_X, *(dest+0), data);
|
|
writeXYZW<tParam>(OFFSET_Y, *(dest+1), data);
|
|
writeXYZW<tParam>(OFFSET_Z, *(dest+2), data);
|
|
writeXYZW<tParam>(OFFSET_W, *(dest+3), data);
|
|
}
|
|
|
|
// The PS2 console actually writes v1v0v1v0 for all V2 unpacks -- the second v1v0 pair
|
|
// being officially "indeterminate" but some games very much depend on it.
|
|
template < uint idx, uint mode, bool doMask, class T >
|
|
static void UNPACK_V2(u32* dest, const T* src)
|
|
{
|
|
writeXYZW<tParam>(OFFSET_X, *(dest+0), *(src+0));
|
|
writeXYZW<tParam>(OFFSET_Y, *(dest+1), *(src+1));
|
|
writeXYZW<tParam>(OFFSET_Z, *(dest+2), *(src+0));
|
|
writeXYZW<tParam>(OFFSET_W, *(dest+3), *(src+1));
|
|
}
|
|
|
|
// V3 and V4 unpacks both use the V4 unpack logic, even though most of the OFFSET_W fields
|
|
// during V3 unpacking end up being overwritten by the next unpack. This is confirmed real
|
|
// hardware behavior that games such as Ape Escape 3 depend on.
|
|
template < uint idx, uint mode, bool doMask, class T >
|
|
static void UNPACK_V4(u32* dest, const T* src)
|
|
{
|
|
writeXYZW<tParam>(OFFSET_X, *(dest+0), *(src+0));
|
|
writeXYZW<tParam>(OFFSET_Y, *(dest+1), *(src+1));
|
|
writeXYZW<tParam>(OFFSET_Z, *(dest+2), *(src+2));
|
|
writeXYZW<tParam>(OFFSET_W, *(dest+3), *(src+3));
|
|
}
|
|
|
|
// V4_5 unpacks do not support the MODE register, and act as mode==0 always.
|
|
template< uint idx, bool doMask >
|
|
static void UNPACK_V4_5(u32 *dest, const u32* src)
|
|
{
|
|
u32 data = *src;
|
|
|
|
writeXYZW<idx,0,doMask>(OFFSET_X, *(dest+0), ((data & 0x001f) << 3));
|
|
writeXYZW<idx,0,doMask>(OFFSET_Y, *(dest+1), ((data & 0x03e0) >> 2));
|
|
writeXYZW<idx,0,doMask>(OFFSET_Z, *(dest+2), ((data & 0x7c00) >> 7));
|
|
writeXYZW<idx,0,doMask>(OFFSET_W, *(dest+3), ((data & 0x8000) >> 8));
|
|
}
|
|
|
|
static void UNPACK_INVALID(u32* dest, const u32* src)
|
|
{
|
|
Console.Warning("Vpu/Vif: Invalid Unpack");
|
|
}
|
|
|
|
// =====================================================================================================
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// Main table for function unpacking.
|
|
// --------------------------------------------------------------------------------------
|
|
// The extra data bsize/dsize/etc are all duplicated between the doMask enabled and
|
|
// disabled versions. This is probably simpler and more efficient than bothering
|
|
// to generate separate tables.
|
|
//
|
|
// The double-cast function pointer nonsense is to appease GCC, which gives some rather
|
|
// cryptic error about being unable to deduce the type parameters (I think it's a bug
|
|
// relating to __fastcall, which I recall having some other places as well). It's fixed
|
|
// by explicitly casting the function to itself prior to casting it to what we need it
|
|
// to be cast as. --air
|
|
//
|
|
|
|
#define _upk (UNPACKFUNCTYPE)
|
|
#define _unpk(usn, bits) (UNPACKFUNCTYPE_##usn##bits)
|
|
#define _unpk_invalid (UNPACKFUNCTYPE)UNPACK_INVALID
|
|
|
|
#define UnpackFuncSet( vt, idx, mode, usn, doMask ) \
|
|
(UNPACKFUNCTYPE)_unpk(u,32) UNPACK_##vt<idx, mode, doMask, u32>, \
|
|
(UNPACKFUNCTYPE)_unpk(usn,16) UNPACK_##vt<idx, mode, doMask, usn##16>, \
|
|
(UNPACKFUNCTYPE)_unpk(usn,8) UNPACK_##vt<idx, mode, doMask, usn##8> \
|
|
|
|
#define UnpackV4_5set(idx, doMask) \
|
|
(UNPACKFUNCTYPE)_unpk(u,32) UNPACK_V4_5<idx, doMask> \
|
|
|
|
#define UnpackModeSet(idx, mode) \
|
|
UnpackFuncSet( S, idx, mode, s, 0 ), _unpk_invalid, \
|
|
UnpackFuncSet( V2, idx, mode, s, 0 ), _unpk_invalid, \
|
|
UnpackFuncSet( V4, idx, mode, s, 0 ), _unpk_invalid, \
|
|
UnpackFuncSet( V4, idx, mode, s, 0 ), UnpackV4_5set(idx, 0), \
|
|
\
|
|
UnpackFuncSet( S, idx, mode, s, 1 ), _unpk_invalid, \
|
|
UnpackFuncSet( V2, idx, mode, s, 1 ), _unpk_invalid, \
|
|
UnpackFuncSet( V4, idx, mode, s, 1 ), _unpk_invalid, \
|
|
UnpackFuncSet( V4, idx, mode, s, 1 ), UnpackV4_5set(idx, 1), \
|
|
\
|
|
UnpackFuncSet( S, idx, mode, u, 0 ), _unpk_invalid, \
|
|
UnpackFuncSet( V2, idx, mode, u, 0 ), _unpk_invalid, \
|
|
UnpackFuncSet( V4, idx, mode, u, 0 ), _unpk_invalid, \
|
|
UnpackFuncSet( V4, idx, mode, u, 0 ), UnpackV4_5set(idx, 0), \
|
|
\
|
|
UnpackFuncSet( S, idx, mode, u, 1 ), _unpk_invalid, \
|
|
UnpackFuncSet( V2, idx, mode, u, 1 ), _unpk_invalid, \
|
|
UnpackFuncSet( V4, idx, mode, u, 1 ), _unpk_invalid, \
|
|
UnpackFuncSet( V4, idx, mode, u, 1 ), UnpackV4_5set(idx, 1)
|
|
|
|
alignas(16) const UNPACKFUNCTYPE VIFfuncTable[2][4][4 * 4 * 2 * 2] =
|
|
{
|
|
{
|
|
{ UnpackModeSet(0,0) },
|
|
{ UnpackModeSet(0,1) },
|
|
{ UnpackModeSet(0,2) },
|
|
{ UnpackModeSet(0,3) }
|
|
},
|
|
|
|
{
|
|
{ UnpackModeSet(1,0) },
|
|
{ UnpackModeSet(1,1) },
|
|
{ UnpackModeSet(1,2) },
|
|
{ UnpackModeSet(1,3) }
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------------
|
|
// Unpack Setup Code
|
|
//----------------------------------------------------------------------------
|
|
|
|
_vifT void vifUnpackSetup(const u32 *data) {
|
|
|
|
vifStruct& vifX = GetVifX;
|
|
|
|
GetVifX.unpackcalls++;
|
|
|
|
if (GetVifX.unpackcalls > 3)
|
|
{
|
|
vifExecQueue(idx);
|
|
}
|
|
//if (!idx) vif0FLUSH(); // Only VU0?
|
|
|
|
vifX.usn = (vifXRegs.code >> 14) & 0x01;
|
|
int vifNum = (vifXRegs.code >> 16) & 0xff;
|
|
|
|
if (vifNum == 0) vifNum = 256;
|
|
vifXRegs.num = vifNum;
|
|
|
|
// This is for use when XGKick is synced as VIF can overwrite XG Kick data as it's transferring out
|
|
// Test with Aggressive Inline Skating, or K-1 Premium 2005 Dynamite!
|
|
// VU currently flushes XGKICK on VU1 end so no need for this, yet
|
|
/*if (idx == 1 && VU1.xgkickenable && !(VU0.VI[REG_TPC].UL & 0x100))
|
|
{
|
|
// Catch up first, then the unpack cycles
|
|
_vuXGKICKTransfer(cpuRegs.cycle - VU1.xgkicklastcycle, false);
|
|
_vuXGKICKTransfer(vifNum * 2, false);
|
|
}*/
|
|
|
|
// Traditional-style way of calculating the gsize, based on VN/VL parameters.
|
|
// Useful when VN/VL are known template params, but currently they are not so we use
|
|
// the LUT instead (for now).
|
|
//uint vl = vifX.cmd & 0x03;
|
|
//uint vn = (vifX.cmd >> 2) & 0x3;
|
|
//uint gsize = ((32 >> vl) * (vn+1)) / 8;
|
|
|
|
const u8& gsize = nVifT[vifX.cmd & 0x0f];
|
|
|
|
uint wl = vifXRegs.cycle.wl ? vifXRegs.cycle.wl : 256;
|
|
|
|
if (wl <= vifXRegs.cycle.cl) { //Skipping write
|
|
vifX.tag.size = ((vifNum * gsize) + 3) / 4;
|
|
}
|
|
else { //Filling write
|
|
int n = vifXRegs.cycle.cl * (vifNum / wl) +
|
|
_limit(vifNum % wl, vifXRegs.cycle.cl);
|
|
|
|
vifX.tag.size = ((n * gsize) + 3) >> 2;
|
|
}
|
|
|
|
u32 addr = vifXRegs.code;
|
|
if (idx && ((addr>>15)&1)) addr += vif1Regs.tops;
|
|
vifX.tag.addr = (addr<<4) & (idx ? 0x3ff0 : 0xff0);
|
|
|
|
VIF_LOG("Unpack VIF%x, QWC %x tagsize %x", idx, vifNum, vifX.tag.size);
|
|
|
|
vifX.cl = 0;
|
|
vifX.tag.cmd = vifX.cmd;
|
|
GetVifX.pass = 1;
|
|
|
|
//Ugh things are never easy.
|
|
//Alright, in most cases with V2 and V3 we only need to know if its offset 32bits.
|
|
//However in V3-16 if the data it requires ends on a QW boundary of the source data
|
|
//the W vector becomes 0, so we need to know how far through the current QW the data begins.
|
|
//same happens with V3 8
|
|
vifX.start_aligned = 4-((vifX.vifpacketsize-1) & 0x3);
|
|
//DevCon.Warning("Aligned %d packetsize at data start %d", vifX.start_aligned, vifX.vifpacketsize - 1);
|
|
}
|
|
|
|
template void vifUnpackSetup<0>(const u32 *data);
|
|
template void vifUnpackSetup<1>(const u32 *data);
|
|
|
|
alignas(16) nVifStruct nVif[2];
|
|
|
|
// Interpreter-style SSE unpacks. Array layout matches the interpreter C unpacks.
|
|
// ([USN][Masking][Unpack Type]) [curCycle]
|
|
alignas(16) nVifCall nVifUpk[(2 * 2 * 16) * 4];
|
|
|
|
// This is used by the interpreted SSE unpacks only. Recompiled SSE unpacks
|
|
// and the interpreted C unpacks use the vif.MaskRow/MaskCol members directly.
|
|
// [MaskNumber][CycleNumber][Vector]
|
|
alignas(16) u32 nVifMask[3][4][4] = {};
|
|
|
|
// Number of bytes of data in the source stream needed for each vector.
|
|
// [equivalent to ((32 >> VL) * (VN+1)) / 8]
|
|
alignas(16) const u8 nVifT[16] = {
|
|
4, // S-32
|
|
2, // S-16
|
|
1, // S-8
|
|
0, // ----
|
|
8, // V2-32
|
|
4, // V2-16
|
|
2, // V2-8
|
|
0, // ----
|
|
12,// V3-32
|
|
6, // V3-16
|
|
3, // V3-8
|
|
0, // ----
|
|
16,// V4-32
|
|
8, // V4-16
|
|
4, // V4-8
|
|
2, // V4-5
|
|
};
|
|
|
|
// ----------------------------------------------------------------------------
|
|
template <int idx, bool doMode, bool isFill>
|
|
__ri void _nVifUnpackLoop(const u8* data);
|
|
|
|
typedef void FnType_VifUnpackLoop(const u8* data);
|
|
typedef FnType_VifUnpackLoop* Fnptr_VifUnpackLoop;
|
|
|
|
// Unpacks Until 'Num' is 0
|
|
alignas(16) static const Fnptr_VifUnpackLoop UnpackLoopTable[2][2][2] = {
|
|
{
|
|
{_nVifUnpackLoop<0, 0, 0>, _nVifUnpackLoop<0, 0, 1>},
|
|
{_nVifUnpackLoop<0, 1, 0>, _nVifUnpackLoop<0, 1, 1>},
|
|
},
|
|
{
|
|
{_nVifUnpackLoop<1, 0, 0>, _nVifUnpackLoop<1, 0, 1>},
|
|
{_nVifUnpackLoop<1, 1, 0>, _nVifUnpackLoop<1, 1, 1>},
|
|
},
|
|
};
|
|
// ----------------------------------------------------------------------------
|
|
|
|
void resetNewVif(int idx)
|
|
{
|
|
// Safety Reset : Reassign all VIF structure info, just in case the VU1 pointers have
|
|
// changed for some reason.
|
|
|
|
nVif[idx].idx = idx;
|
|
nVif[idx].bSize = 0;
|
|
std::memset(nVif[idx].buffer, 0, sizeof(nVif[idx].buffer));
|
|
|
|
if (CanUseVifDynarec())
|
|
dVifReset(idx);
|
|
}
|
|
|
|
void releaseNewVif(int idx)
|
|
{
|
|
}
|
|
|
|
static __fi u8* getVUptr(uint idx, int offset)
|
|
{
|
|
return (u8*)(vuRegs[idx].Mem + (offset & (idx ? 0x3ff0 : 0xff0)));
|
|
}
|
|
|
|
|
|
_vifT int nVifUnpack(const u8* data)
|
|
{
|
|
nVifStruct& v = nVif[idx];
|
|
vifStruct& vif = GetVifX;
|
|
VIFregisters& vifRegs = vifXRegs;
|
|
|
|
const uint wl = vifRegs.cycle.wl ? vifRegs.cycle.wl : 256;
|
|
const uint ret = std::min(vif.vifpacketsize, vif.tag.size);
|
|
const bool isFill = (vifRegs.cycle.cl < wl);
|
|
s32 size = ret << 2;
|
|
|
|
if (ret == vif.tag.size) // Full Transfer
|
|
{
|
|
if (v.bSize) // Last transfer was partial
|
|
{
|
|
memcpy(&v.buffer[v.bSize], data, size);
|
|
v.bSize += size;
|
|
size = v.bSize;
|
|
data = v.buffer;
|
|
|
|
vif.cl = 0;
|
|
vifRegs.num = (vifXRegs.code >> 16) & 0xff; // grab NUM form the original VIFcode input.
|
|
if (!vifRegs.num)
|
|
vifRegs.num = 256;
|
|
}
|
|
|
|
if (!idx || !THREAD_VU1)
|
|
{
|
|
if (CanUseVifDynarec())
|
|
dVifUnpack<idx>(data, isFill);
|
|
else
|
|
_nVifUnpack(idx, data, vifRegs.mode, isFill);
|
|
}
|
|
else
|
|
vu1Thread.VifUnpack(vif, vifRegs, (u8*)data, (size + 4) & ~0x3);
|
|
|
|
vif.pass = 0;
|
|
vif.tag.size = 0;
|
|
vif.cmd = 0;
|
|
vifRegs.num = 0;
|
|
v.bSize = 0;
|
|
}
|
|
else // Partial Transfer
|
|
{
|
|
memcpy(&v.buffer[v.bSize], data, size);
|
|
v.bSize += size;
|
|
vif.tag.size -= ret;
|
|
|
|
const u8& vSize = nVifT[vif.cmd & 0x0f];
|
|
|
|
// We need to provide accurate accounting of the NUM register, in case games decided
|
|
// to read back from it mid-transfer. Since so few games actually use partial transfers
|
|
// of VIF unpacks, this code should not be any bottleneck.
|
|
|
|
if (!isFill)
|
|
{
|
|
vifRegs.num -= (size / vSize);
|
|
}
|
|
else
|
|
{
|
|
int dataSize = (size / vSize);
|
|
vifRegs.num = vifRegs.num - (((dataSize / vifRegs.cycle.cl) * (vifRegs.cycle.wl - vifRegs.cycle.cl)) + dataSize);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
template int nVifUnpack<0>(const u8* data);
|
|
template int nVifUnpack<1>(const u8* data);
|
|
|
|
// This is used by the interpreted SSE unpacks only. Recompiled SSE unpacks
|
|
// and the interpreted C unpacks use the vif.MaskRow/MaskCol members directly.
|
|
static void setMasks(const vifStruct& vif, const VIFregisters& v)
|
|
{
|
|
for (int i = 0; i < 16; i++)
|
|
{
|
|
int m = (v.mask >> (i * 2)) & 3;
|
|
switch (m)
|
|
{
|
|
case 0: // Data
|
|
nVifMask[0][i / 4][i % 4] = 0xffffffff;
|
|
nVifMask[1][i / 4][i % 4] = 0;
|
|
nVifMask[2][i / 4][i % 4] = 0;
|
|
break;
|
|
case 1: // MaskRow
|
|
nVifMask[0][i / 4][i % 4] = 0;
|
|
nVifMask[1][i / 4][i % 4] = 0;
|
|
nVifMask[2][i / 4][i % 4] = vif.MaskRow._u32[i % 4];
|
|
break;
|
|
case 2: // MaskCol
|
|
nVifMask[0][i / 4][i % 4] = 0;
|
|
nVifMask[1][i / 4][i % 4] = 0;
|
|
nVifMask[2][i / 4][i % 4] = vif.MaskCol._u32[i / 4];
|
|
break;
|
|
case 3: // Write Protect
|
|
nVifMask[0][i / 4][i % 4] = 0;
|
|
nVifMask[1][i / 4][i % 4] = 0xffffffff;
|
|
nVifMask[2][i / 4][i % 4] = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Unpacking Optimization notes:
|
|
// ----------------------------------------------------------------------------
|
|
// Some games send a LOT of single-cycle packets (God of War, SotC, TriAce games, etc),
|
|
// so we always need to be weary of keeping loop setup code optimized. It's not always
|
|
// a "win" to move code outside the loop, like normally in most other loop scenarios.
|
|
//
|
|
// The biggest bottleneck of the current code is the call/ret needed to invoke the SSE
|
|
// unpackers. A better option is to generate the entire vifRegs.num loop code as part
|
|
// of the SSE template, and inline the SSE code into the heart of it. This both avoids
|
|
// the call/ret and opens the door for resolving some register dependency chains in the
|
|
// current emitted functions. (this is what zero's SSE does to get it's final bit of
|
|
// speed advantage over the new vif). --air
|
|
//
|
|
// The BEST optimizatin strategy here is to use data available to us from the UNPACK dispatch
|
|
// -- namely the unpack type and mask flag -- in combination mode and usn values -- to
|
|
// generate ~600 special versions of this function. But since it's an interpreter, who gives
|
|
// a crap? Really? :p
|
|
//
|
|
|
|
// size - size of the packet fragment incoming from DMAC.
|
|
template <int idx, bool doMode, bool isFill>
|
|
__ri void _nVifUnpackLoop(const u8* data)
|
|
{
|
|
|
|
vifStruct& vif = MTVU_VifX;
|
|
VIFregisters& vifRegs = MTVU_VifXRegs;
|
|
|
|
// skipSize used for skipping writes only
|
|
const int skipSize = (vifRegs.cycle.cl - vifRegs.cycle.wl) * 16;
|
|
|
|
//DevCon.WriteLn("[%d][%d][%d][num=%d][upk=%d][cl=%d][bl=%d][skip=%d]", isFill, doMask, doMode, vifRegs.num, upkNum, vif.cl, blockSize, skipSize);
|
|
|
|
// Mode 0 historically used the generated interpreter-unpack table while
|
|
// modes 1-3 used VIFfuncTable. Without executable code memory that table
|
|
// is not initialized, so mode 0 must use the same precompiled C path too.
|
|
const bool useGeneratedModeZero = !doMode && CanUseVifDynarec();
|
|
if (useGeneratedModeZero && (vif.cmd & 0x10))
|
|
setMasks(vif, vifRegs);
|
|
|
|
const int usn = !!vif.usn;
|
|
const int upkNum = vif.cmd & 0x1f;
|
|
const u8& vSize = nVifT[upkNum & 0x0f];
|
|
//uint vl = vif.cmd & 0x03;
|
|
//uint vn = (vif.cmd >> 2) & 0x3;
|
|
//uint vSize = ((32 >> vl) * (vn+1)) / 8; // size of data (in bytes) used for each write cycle
|
|
|
|
const nVifCall* fnbase = useGeneratedModeZero ?
|
|
&nVifUpk[((usn * 2 * 16) + upkNum) * (4 * 1)] : nullptr;
|
|
const UNPACKFUNCTYPE ft = VIFfuncTable[idx][doMode ? vifRegs.mode : 0][((usn * 2 * 16) + upkNum)];
|
|
|
|
pxAssume(vif.cl == 0);
|
|
//pxAssume (vifRegs.cycle.wl > 0);
|
|
|
|
do
|
|
{
|
|
u8* dest = getVUptr(idx, vif.tag.addr);
|
|
|
|
if (!useGeneratedModeZero)
|
|
{
|
|
ft(dest, data);
|
|
}
|
|
else
|
|
{
|
|
uint cl3 = std::min(vif.cl, 3);
|
|
fnbase[cl3](dest, data);
|
|
}
|
|
|
|
vif.tag.addr += 16;
|
|
--vifRegs.num;
|
|
++vif.cl;
|
|
|
|
if (isFill)
|
|
{
|
|
//DevCon.WriteLn("isFill!");
|
|
if (vif.cl <= vifRegs.cycle.cl)
|
|
data += vSize;
|
|
else if (vif.cl == vifRegs.cycle.wl)
|
|
vif.cl = 0;
|
|
}
|
|
else
|
|
{
|
|
data += vSize;
|
|
|
|
if (vif.cl >= vifRegs.cycle.wl)
|
|
{
|
|
vif.tag.addr += skipSize;
|
|
vif.cl = 0;
|
|
}
|
|
}
|
|
} while (vifRegs.num);
|
|
}
|
|
|
|
__fi void _nVifUnpack(int idx, const u8* data, uint mode, bool isFill)
|
|
{
|
|
UnpackLoopTable[idx][!!mode][isFill](data);
|
|
}
|