Files
ARMSX2/pcsx2/Cache.cpp
Brian Degenhardt aeabe3e0e5 EE cache: fix the DXSTG tag lookup's 29-bit fold, and the tests around it (#568)
* EE: the D-cache store-tag lookup dropped the top three bits of the tag

DXSTG takes a guest physical page from TagLo and has to turn it into the
host pointer our tags carry. It did that by routing the page through its
KSEG0 alias, which meant masking the tag to 29 bits first -- and KSEG0 is
only 512 MB wide, so the mask was not a formality. Every physical page at
or above 0x20000000 folded into the low half of the map and resolved to
whatever happened to live at the folded address.

The consequence that matters is that a page past the end of the physical
map folded onto real memory: 0x60129000 resolved to 0x00129000, and the
eviction wrote 64 bytes of cache line into guest RAM the tag never named.

Use vtlb_GetPhyPtr instead, which is what the debugger and PSM already use
to ask this question. It covers the whole 1 GB physical map and answers
null both for a handler page and for an address off the end of the map, so
the unbacked case is now decided by the same lookup that produces the
pointer rather than by a truncation.

Where a tag naming one of our main-RAM mirrors resolves changes as a side
effect of that, and is deliberately left unpinned. Those mirrors are our
physical map's, not a console's: an SCPH-30001 has no RAM at those physical
addresses, and an eviction steered at one reached nothing at all. There is
no hardware answer to hold us to, so nothing asserts one.

* Tests: point the DXSTG unresolvable-page check at a page that is unresolvable

The check named 0x1FFFF000, described as "BIOS/unmapped territory at the
top of the physical map". That page is the last one of the 4 MB BIOS ROM
mapped at 0x1FC00000, so it is real backing memory: the test took the
backed branch every time, wrote 64 bytes into the loaded BIOS image, and
asserted only that nothing faulted. The branch it was named for -- the one
carrying the safety property -- had no coverage at all.

Name 0x60129000 instead. It is past the end of the physical map, and it is
the page with teeth, because the old 29-bit fold sent it to 0x00129000 in
main RAM. A witness there turns "we did not fault" into "we did not write
somewhere the guest never named", which is the property worth holding.

An SCPH-30001 agrees with that much: an eviction steered above the end of
RAM puts nothing into RAM. Nothing beyond it is asserted -- where a tag
naming one of our main-RAM mirrors resolves is emulator-specific, so it
stays unpinned, with a comment saying so and why.

* Tests: stop the DXSTG write-back check skipping on 16K-page hosts

MapAt's candidate addresses are 4K-aligned and none is 16K-aligned, so on
a 16K-page kernel -- Asahi, Apple Silicon, some Android, and one of our own
CI jobs -- the kernel rejects every one of them and the mapping fails. The
write-back check treated that as a precondition and skipped outright, which
took its guest-side assertions with it: the ones that actually pin where a
DXSTG-steered eviction lands, none of which need anything from the host.

The mapping is only the negative control, there to show the write-back did
not ALSO reach the host page carrying the same number. Make it optional.
The guest-side half now runs everywhere and only the control drops out.

DxstgDirtyStaysInsideGuestMemory still skips, and should: it is entirely
about the host page. That leaves one skip here on a 16K-page host instead
of two, and none at all on a 4K one.
2026-08-14 21:50:24 -07:00

541 lines
14 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Common.h"
#include "Cache.h"
#include "vtlb.h"
using namespace R5900;
using namespace vtlb_private;
namespace
{
union alignas(64) CacheData
{
u8 bytes[64];
};
struct CacheTag
{
uptr rawValue;
// You are able to configure a TLB entry with non-existant physical address without causing a bus error.
// When this happens, the cache still fills with the data and when it gets evicted the data is lost.
// We don't emulate memory access on a logic level, so we need to ensure that we don't try to load/store to a non-existant physical address.
// This fixes the Find My Own Way demo.
// The lower parts of a cache tags structure is as follows:
// 31 - 12: The physical address cache tag.
// 11: Used by PCSX2 to indicate if the physical address is valid.
// 10 - 7: Unused.
// 6: Dirty flag.
// 5: Valid flag.
// 4: LRF flag - least recently filled flag.
// 3: Lock flag.
// 2-0: Unused.
enum Flags : decltype(rawValue)
{
DIRTY_FLAG = 0x40,
VALID_FLAG = 0x20,
LRF_FLAG = 0x10,
LOCK_FLAG = 0x8,
ALL_FLAGS = 0x7FF,
ALL_BITS = 0xFFF
};
int flags() const
{
return rawValue & ALL_FLAGS;
}
bool isValid() const { return rawValue & VALID_FLAG; }
bool isDirty() const { return rawValue & DIRTY_FLAG; }
bool lrf() const { return rawValue & LRF_FLAG; }
bool isLocked() const { return rawValue & LOCK_FLAG; }
bool isDirtyAndValid() const
{
return (rawValue & (DIRTY_FLAG | VALID_FLAG)) == (DIRTY_FLAG | VALID_FLAG);
}
void setValid() { rawValue |= VALID_FLAG; }
void setDirty() { rawValue |= DIRTY_FLAG; }
void setLocked() { rawValue |= LOCK_FLAG; }
void clearValid() { rawValue &= ~VALID_FLAG; }
void clearDirty() { rawValue &= ~DIRTY_FLAG; }
void clearLocked() { rawValue &= ~LOCK_FLAG; }
void toggleLRF() { rawValue ^= LRF_FLAG; }
uptr addr() const { return rawValue & ~ALL_BITS; }
void setAddr(uptr addr)
{
rawValue &= ALL_BITS;
rawValue |= (addr & ~ALL_BITS);
}
bool matches(uptr other) const
{
return isValid() && addr() == (other & ~ALL_BITS);
}
void clear()
{
rawValue &= LRF_FLAG;
}
constexpr bool isValidPFN() const
{
return rawValue & 0x800;
}
constexpr void setValidPFN(bool valid)
{
if (valid)
rawValue |= 0x800;
else
rawValue &= ~0x800;
}
};
struct CacheLine
{
CacheTag& tag;
CacheData& data;
int set;
uptr addr()
{
return tag.addr() | (set << 6);
}
void writeBackIfNeeded()
{
if (!tag.isDirtyAndValid())
return;
uptr target = addr();
CACHE_LOG("Write back at %zx", target);
if (tag.isValidPFN())
*reinterpret_cast<CacheData*>(target) = data;
tag.clearDirty();
}
void load(uptr ppf)
{
pxAssertMsg(!tag.isDirtyAndValid(), "Loaded a value into cache without writing back the old one!");
tag.setAddr(ppf);
if (!tag.isValidPFN())
{
// Reading from invalid physical addresses seems to return 0 on hardware
std::memset(&data, 0, sizeof(data));
}
else
{
std::memcpy(&data, reinterpret_cast<void*>(ppf & ~0x3FULL), sizeof(data));
}
tag.setValid();
tag.clearDirty();
}
void clear()
{
tag.clear();
std::memset(&data, 0, sizeof(data));
}
};
struct CacheSet
{
CacheTag tags[2];
CacheData data[2];
};
struct Cache
{
CacheSet sets[64];
int setIdxFor(u32 vaddr) const
{
return (vaddr >> 6) & 0x3F;
}
CacheLine lineAt(int idx, int way)
{
return {sets[idx].tags[way], sets[idx].data[way], idx};
}
};
static Cache cache = {};
} // namespace
void resetCache()
{
std::memset(&cache, 0, sizeof(cache));
}
void writebackCache()
{
for (int i = 0; i < 64; i++)
{
for (int j = 0; j < 2; j++)
{
cache.lineAt(i, j).writeBackIfNeeded();
}
}
}
static bool findInCache(const CacheSet& set, uptr ppf, int* way)
{
auto check = [&](int checkWay) -> bool {
if (!set.tags[checkWay].matches(ppf))
return false;
*way = checkWay;
return true;
};
return check(0) || check(1);
}
static int getFreeCache(u32 mem, int* way, bool validPFN)
{
const int setIdx = cache.setIdxFor(mem);
CacheSet& set = cache.sets[setIdx];
VTLBVirtual vmv = vtlbdata.vmap[mem >> VTLB_PAGE_BITS];
*way = set.tags[0].lrf() ^ set.tags[1].lrf();
if (validPFN)
pxAssertMsg(!vmv.isHandler(mem), "Cache currently only supports non-handler addresses!");
uptr ppf = vmv.assumePtr(mem);
[[unlikely]]
if ((cpuRegs.CP0.n.Config & 0x10000) == 0)
CACHE_LOG("Cache off!");
if (findInCache(set, ppf, way))
{
[[unlikely]]
if (set.tags[*way].isLocked())
{
// Check the other way
if (set.tags[*way ^ 1].isLocked())
{
Console.Error("CACHE: SECOND WAY IS LOCKED.", setIdx, *way);
}
else
{
// Force the unlocked way
*way ^= 1;
}
}
}
else
{
int newWay = set.tags[0].lrf() ^ set.tags[1].lrf();
[[unlikely]]
if (set.tags[newWay].isLocked())
{
// If the new way is locked, we force the unlocked way, ignoring the lrf bits.
newWay = newWay ^ 1;
[[unlikely]]
if (set.tags[newWay].isLocked())
{
Console.Warning("CACHE: SECOND WAY IS LOCKED.", setIdx, *way);
}
}
*way = newWay;
CacheLine line = cache.lineAt(setIdx, newWay);
line.writeBackIfNeeded();
line.tag.setValidPFN(validPFN);
line.load(ppf);
line.tag.toggleLRF();
}
return setIdx;
}
template <bool Write, int Bytes>
void* prepareCacheAccess(u32 mem, int* way, int* idx, bool validPFN = true)
{
*way = 0;
*idx = getFreeCache(mem, way, validPFN);
CacheLine line = cache.lineAt(*idx, *way);
if (Write)
line.tag.setDirty();
u32 aligned = mem & ~(Bytes - 1);
return &line.data.bytes[aligned & 0x3f];
}
template <typename Int>
void writeCache(u32 mem, Int value, bool validPFN)
{
int way, idx;
void* addr = prepareCacheAccess<true, sizeof(Int)>(mem, &way, &idx, validPFN);
CACHE_LOG("writeCache%d %8.8x adding to %d, way %d, value %llx", 8 * sizeof(value), mem, idx, way, value);
*reinterpret_cast<Int*>(addr) = value;
}
void writeCache8(u32 mem, u8 value, bool validPFN)
{
writeCache<u8>(mem, value, validPFN);
}
void writeCache16(u32 mem, u16 value, bool validPFN)
{
writeCache<u16>(mem, value, validPFN);
}
void writeCache32(u32 mem, u32 value, bool validPFN)
{
writeCache<u32>(mem, value, validPFN);
}
void writeCache64(u32 mem, const u64 value, bool validPFN)
{
writeCache<u64>(mem, value, validPFN);
}
void writeCache128(u32 mem, const mem128_t* value, bool validPFN)
{
int way, idx;
void* addr = prepareCacheAccess<true, sizeof(mem128_t)>(mem, &way, &idx, validPFN);
CACHE_LOG("writeCache128 %8.8x adding to %d, way %x, lo %llx, hi %llx", mem, idx, way, value->lo, value->hi);
*reinterpret_cast<mem128_t*>(addr) = *value;
}
template <typename Int>
Int readCache(u32 mem, bool validPFN)
{
int way, idx;
void* addr = prepareCacheAccess<false, sizeof(Int)>(mem, &way, &idx, validPFN);
Int value = *reinterpret_cast<Int*>(addr);
CACHE_LOG("readCache%d %8.8x from %d, way %d, value %llx", 8 * sizeof(value), mem, idx, way, value);
return value;
}
u8 readCache8(u32 mem, bool validPFN)
{
return readCache<u8>(mem, validPFN);
}
u16 readCache16(u32 mem, bool validPFN)
{
return readCache<u16>(mem, validPFN);
}
u32 readCache32(u32 mem, bool validPFN)
{
return readCache<u32>(mem, validPFN);
}
u64 readCache64(u32 mem, bool validPFN)
{
return readCache<u64>(mem, validPFN);
}
RETURNS_R128 readCache128(u32 mem, bool validPFN)
{
int way, idx;
void* addr = prepareCacheAccess<false, sizeof(mem128_t)>(mem, &way, &idx, validPFN);
r128 value = r128_load(addr);
u64* vptr = reinterpret_cast<u64*>(&value);
CACHE_LOG("readCache128 %8.8x from %d, way %d, lo %llx, hi %llx", mem, idx, way, vptr[0], vptr[1]);
return value;
}
template <typename Op>
void doCacheHitOp(u32 addr, const char* name, Op op)
{
const int index = cache.setIdxFor(addr);
CacheSet& set = cache.sets[index];
VTLBVirtual vmv = vtlbdata.vmap[addr >> VTLB_PAGE_BITS];
uptr ppf = vmv.assumePtr(addr);
int way;
if (!findInCache(set, ppf, &way))
{
CACHE_LOG("CACHE %s NO HIT addr %x, index %d, tag0 %zx tag1 %zx", name, addr, index, set.tags[0].rawValue, set.tags[1].rawValue);
return;
}
CACHE_LOG("CACHE %s addr %x, index %d, way %d, flags %x OP %x", name, addr, index, way, set.tags[way].flags(), cpuRegs.code);
op(cache.lineAt(index, way));
}
namespace R5900
{
namespace Interpreter
{
namespace OpcodeImpl
{
extern int Dcache;
void CACHE()
{
u32 addr = cpuRegs.GPR.r[_Rs_].UL[0] + _Imm_;
// CACHE_LOG("cpuRegs.GPR.r[_Rs_].UL[0] = %x, IMM = %x RT = %x", cpuRegs.GPR.r[_Rs_].UL[0], _Imm_, _Rt_);
switch (_Rt_)
{
case 0x1a: //DHIN (Data Cache Hit Invalidate)
doCacheHitOp(addr, "DHIN", [](CacheLine line) {
line.clear();
});
break;
case 0x18: //DHWBIN (Data Cache Hit WriteBack with Invalidate)
doCacheHitOp(addr, "DHWBIN", [](CacheLine line) {
line.writeBackIfNeeded();
line.clear();
});
break;
case 0x1c: //DHWOIN (Data Cache Hit WriteBack Without Invalidate)
doCacheHitOp(addr, "DHWOIN", [](CacheLine line) {
line.writeBackIfNeeded();
});
break;
case 0x16: //DXIN (Data Cache Index Invalidate)
{
const int index = cache.setIdxFor(addr);
const int way = addr & 0x1;
CacheLine line = cache.lineAt(index, way);
CACHE_LOG("CACHE DXIN addr %x, index %d, way %d, flag %x", addr, index, way, line.tag.flags());
line.clear();
break;
}
case 0x11: //DXLDT (Data Cache Load Data into TagLo)
{
const int index = cache.setIdxFor(addr);
const int way = addr & 0x1;
CacheLine line = cache.lineAt(index, way);
cpuRegs.CP0.n.TagLo = *reinterpret_cast<u32*>(&line.data.bytes[addr & 0x3C]);
CACHE_LOG("CACHE DXLDT addr %x, index %d, way %d, DATA %x OP %x", addr, index, way, cpuRegs.CP0.n.TagLo, cpuRegs.code);
break;
}
case 0x10: //DXLTG (Data Cache Load Tag into TagLo)
{
const int index = (addr >> 6) & 0x3F;
const int way = addr & 0x1;
CacheLine line = cache.lineAt(index, way);
// DXLTG demands that SYNC.L is called before this command, which forces the cache to write back, so presumably games are checking the cache has updated the memory
// For speed, we will do it here.
line.writeBackIfNeeded();
// Our tags don't contain PS2 paddrs (instead they contain x86 addrs)
cpuRegs.CP0.n.TagLo = line.tag.flags();
CACHE_LOG("CACHE DXLTG addr %x, index %d, way %d, DATA %x OP %x ", addr, index, way, cpuRegs.CP0.n.TagLo, cpuRegs.code);
CACHE_LOG("WARNING: DXLTG emulation supports flags only, things could break");
break;
}
case 0x13: //DXSDT (Data Cache Store 32bits from TagLo)
{
const int index = (addr >> 6) & 0x3F;
const int way = addr & 0x1;
CacheLine line = cache.lineAt(index, way);
*reinterpret_cast<u32*>(&line.data.bytes[addr & 0x3C]) = cpuRegs.CP0.n.TagLo;
CACHE_LOG("CACHE DXSDT addr %x, index %d, way %d, DATA %x OP %x", addr, index, way, cpuRegs.CP0.n.TagLo, cpuRegs.code);
break;
}
case 0x12: //DXSTG (Data Cache Store Tag from TagLo)
{
const int index = (addr >> 6) & 0x3F;
const int way = addr & 0x1;
CacheLine line = cache.lineAt(index, way);
// TagLo carries a guest physical page. Our tags do not: they hold the
// host pointer the fill translated to (CacheLine::load stores `ppf`),
// which is what writeBackIfNeeded dereferences, so copying the guest
// word in raw aimed a 64-byte store at an address the guest chose --
// setAddr zeroes the top 32 bits, so somewhere below 4 GiB: an
// emulator crash normally, or a write into whatever happened to be
// mapped there. Translate it the way a fill does instead, through the
// KSEG0 alias of the physical page (Memory.cpp maps 0x80000000 onto
// physical 0), so the write-back lands at the physical address the
// guest named, and take isValidPFN from the same translation so the
// two cannot disagree. A tag that does not resolve to plain memory --
// an MMIO handler page, or a physical address that does not exist --
// is marked unbacked; the line still caches and reports its flags,
// and loses its data on eviction (see the comment on CacheTag).
//
// The lookup goes through the physical map, not through the KSEG0
// alias of the page: KSEG0 is only 512 MB wide, so routing a
// physical page through it meant masking the tag to 29 bits, and
// every page at or above 0x20000000 then folded into the low half
// of the map and resolved to whatever lives there. A page past the
// end of the map folded onto ordinary RAM and the write-back went
// into it. vtlb_GetPhyPtr covers the whole 1 GB physical map and
// answers null both for a handler page and for an address off the
// end of it.
const u32 pageTag = cpuRegs.CP0.n.TagLo & ~static_cast<u32>(CacheTag::ALL_BITS);
void* const host = vtlb_GetPhyPtr(pageTag);
const bool backed = host != nullptr;
line.tag.setValidPFN(backed);
line.tag.setAddr(backed ? reinterpret_cast<uptr>(host) : static_cast<uptr>(pageTag));
line.tag.rawValue &= ~CacheTag::ALL_FLAGS;
line.tag.rawValue |= (cpuRegs.CP0.n.TagLo & CacheTag::ALL_FLAGS);
CACHE_LOG("CACHE DXSTG addr %x, index %d, way %d, DATA %x OP %x", addr, index, way, cpuRegs.CP0.n.TagLo, cpuRegs.code);
break;
}
case 0x14: //DXWBIN (Data Cache Index WriteBack Invalidate)
{
const int index = (addr >> 6) & 0x3F;
const int way = addr & 0x1;
CacheLine line = cache.lineAt(index, way);
CACHE_LOG("CACHE DXWBIN addr %x, index %d, way %d, flags %x paddr %zx", addr, index, way, line.tag.flags(), line.addr());
line.writeBackIfNeeded();
line.clear();
break;
}
case 0x7: //IXIN (Instruction Cache Index Invalidate)
{
//Not Implemented as we do not have instruction cache
break;
}
case 0xC: //BFH (BTAC Flush)
{
//Not Implemented as we do not cache Branch Target Addresses.
break;
}
default:
DevCon.Warning("Cache mode %x not implemented", _Rt_);
break;
}
}
} // end namespace OpcodeImpl
} // namespace Interpreter
} // namespace R5900