mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-12 01:38:50 -04:00
713 lines
26 KiB
C++
713 lines
26 KiB
C++
#include "gx_internal.h"
|
|
#include "runtime_log.h"
|
|
|
|
#include <cstddef>
|
|
#include <limits>
|
|
|
|
// --- Texture and TLUT Objects ---
|
|
|
|
namespace aurora::gfx {
|
|
struct TextureRef;
|
|
using TextureHandle = std::shared_ptr<TextureRef>;
|
|
}
|
|
|
|
// HleTexObj now lives in gx_internal.h: it is embedded in TexObjSlot so the
|
|
// metadata and the Aurora object share one hash-map entry.
|
|
|
|
struct HostGXTlutObj {
|
|
alignas(GXTlutObj) std::byte publicStorage[sizeof(GXTlutObj)]{};
|
|
aurora::gfx::TextureHandle ref;
|
|
};
|
|
|
|
struct HleTlutObj {
|
|
static constexpr size_t kTlutObjStorageSize =
|
|
(sizeof(GXTlutObj) >= sizeof(HostGXTlutObj)) ? sizeof(GXTlutObj) : sizeof(HostGXTlutObj);
|
|
static constexpr size_t kTlutObjStorageAlign =
|
|
(alignof(GXTlutObj) >= alignof(HostGXTlutObj)) ? alignof(GXTlutObj) : alignof(HostGXTlutObj);
|
|
using Storage = std::aligned_storage_t<kTlutObjStorageSize, kTlutObjStorageAlign>;
|
|
|
|
Storage storage{};
|
|
bool storageLive = false;
|
|
bool constructed = false;
|
|
|
|
HleTlutObj() = default;
|
|
~HleTlutObj() { Destroy(); }
|
|
|
|
HostGXTlutObj* HostObj() { return reinterpret_cast<HostGXTlutObj*>(&storage); }
|
|
GXTlutObj* PublicPtr() { return reinterpret_cast<GXTlutObj*>(HostObj()->publicStorage); }
|
|
|
|
void EnsureStorageLive() {
|
|
if (!storageLive) {
|
|
new (&storage) HostGXTlutObj();
|
|
storageLive = true;
|
|
}
|
|
}
|
|
|
|
void Destroy() {
|
|
if (storageLive) {
|
|
GXDestroyTlutObj(PublicPtr());
|
|
std::destroy_at(HostObj());
|
|
std::memset(&storage, 0, sizeof(storage));
|
|
storageLive = false;
|
|
constructed = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
std::mutex g_texObjMutex;
|
|
std::map<uint32_t, std::unique_ptr<HleTlutObj>> g_HostTlutObjMap;
|
|
std::mutex g_tlutObjMutex;
|
|
|
|
// The texobj table. One slot per guest GXTexObj address holds both the decoded
|
|
// metadata and the Aurora object, so GXLoadTexObj resolves everything it needs
|
|
// with a single hash lookup instead of six tree descents across two maps.
|
|
std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta;
|
|
std::map<uint32_t, TlutObjMeta> g_TlutObjMeta;
|
|
std::array<BoundTexInfo, 8> g_boundTexMaps{};
|
|
|
|
namespace {
|
|
|
|
// A whole GXLoadTexObj call resolves the same guest address several times, so memoising the last
|
|
// slot turns all but the first lookup into a pointer compare. Safe because g_TexObjMeta is
|
|
// node-based and never erased from, so element references stay valid across rehashes.
|
|
uint32_t g_texObjSlotMemoAddr = 0;
|
|
TexObjSlot* g_texObjSlotMemo = nullptr;
|
|
|
|
constexpr size_t kTexObjTableReserve = 1024;
|
|
|
|
} // namespace
|
|
|
|
struct TexObjIntervalEntry {
|
|
uint32_t dataAddr = 0;
|
|
uint32_t size = 0;
|
|
uint32_t objAddr = 0;
|
|
TexObjSlot* slot = nullptr;
|
|
};
|
|
|
|
// DCStoreRange invalidation index, sorted by (dataAddr, objAddr). Rebuilding is O(N log N) plus a
|
|
// GXGetTexBufferSize per texobj, so it must not happen once per DCStoreRange: mutations instead
|
|
// queue their slot for a targeted patch. The queue used to be a capacity-limited vector that
|
|
// overflowed under real per-frame GXLoadTexObj volume and silently degraded into a full rebuild
|
|
// every call; the dedupe flag now lives on the slot so queuing is O(1) and uncapped.
|
|
std::vector<TexObjIntervalEntry> g_texObjIntervals;
|
|
std::vector<TexObjSlot*> g_texObjIntervalsPending;
|
|
bool g_texObjIntervalsRebuildAll = true;
|
|
|
|
// Upper bound on any entry's byte size. The dirty scan walks left from lower_bound(dirtyStart)
|
|
// and needs this to know when no earlier-starting interval can still reach dirtyStart (intervals
|
|
// are sorted by start only, so a larger one could start further back). An overestimate just
|
|
// lengthens the walk, so the patch path only ever raises it and rebuild recomputes it exactly.
|
|
uint32_t g_texObjIntervalMaxSize = 0;
|
|
|
|
// Address bounds of every registered TLUT payload, so a DCStoreRange over an
|
|
// unrelated buffer can skip the g_TlutObjMeta scan entirely. Recomputed lazily
|
|
// because TLUT registration is far rarer than cache flushes.
|
|
uint64_t g_tlutObjBoundsStart = 0;
|
|
uint64_t g_tlutObjBoundsEnd = 0;
|
|
bool g_tlutObjBoundsDirty = true;
|
|
|
|
// Guest GXTexObj is 32 bytes: word0 wrap/filter bits, word1 LOD bits, word2 width/height/format,
|
|
// word3 data address (<<5), word4 user data, word5 texFmt, word6 tlut, byte 0x1F flags. Exact
|
|
// bit positions are decoded inline below.
|
|
|
|
TexObjMeta ExtractTexObjMetaFromGuest(uint32_t addr) {
|
|
TexObjMeta meta{};
|
|
try {
|
|
uint32_t word0 = Memory::Read32(addr + 0x00);
|
|
uint32_t word1 = Memory::Read32(addr + 0x04);
|
|
uint32_t word2 = Memory::Read32(addr + 0x08);
|
|
uint32_t word3 = Memory::Read32(addr + 0x0C);
|
|
uint32_t word4 = Memory::Read32(addr + 0x10);
|
|
uint32_t word5 = Memory::Read32(addr + 0x14);
|
|
uint32_t word6 = Memory::Read32(addr + 0x18);
|
|
uint8_t flags = Memory::Read8(addr + 0x1F);
|
|
|
|
meta.wrapS = word0 & 0x3;
|
|
meta.wrapT = (word0 >> 2) & 0x3;
|
|
meta.width = (word2 & 0x3FF) + 1;
|
|
meta.height = ((word2 >> 10) & 0x3FF) + 1;
|
|
meta.dataAddr = CanonicalizeGxMainRamAddress((word3 & 0xFFFFFF) << 5);
|
|
meta.userData = word4;
|
|
meta.mipmap = (flags & 1) != 0;
|
|
|
|
const uint32_t formatFromWord2 = (word2 >> 20) & 0xF;
|
|
uint32_t resolvedFormat = word5;
|
|
if (!IsKnownTexFormat(resolvedFormat) && IsKnownTexFormat(formatFromWord2)) {
|
|
resolvedFormat = formatFromWord2;
|
|
}
|
|
meta.format = resolvedFormat;
|
|
meta.tlut = word6;
|
|
|
|
// LOD parameters from GXGetTexObjLODAll
|
|
// minLod = ((word1 & 0xff) / 16.0f)
|
|
// maxLod = (((word1 >> 8) & 0xff) / 16.0f)
|
|
meta.minLod = (float)(word1 & 0xFF) / 16.0f;
|
|
meta.maxLod = (float)((word1 >> 8) & 0xFF) / 16.0f;
|
|
|
|
// lodBias from bits [17:9] as signed 8-bit, scaled by 1/32
|
|
int8_t biasRaw = (int8_t)((word0 >> 9) & 0xFF);
|
|
meta.lodBias = (float)biasRaw / 32.0f;
|
|
|
|
// biasClamp from bit 21
|
|
meta.biasClamp = ((word0 >> 21) & 1) != 0;
|
|
|
|
// edgeLod from countLeadingZeros((word0 >> 8) & 1) >> 5
|
|
// If bit 8 is set, edgeLod=0, else edgeLod=1
|
|
meta.edgeLod = ((word0 >> 8) & 1) == 0;
|
|
|
|
// maxAniso from bits [20:19]
|
|
meta.maxAniso = (word0 >> 19) & 0x3;
|
|
|
|
// Filter modes - bits [7:5] store the encoded HW value written by
|
|
// GX2HWFiltConv. This is the inverse of kGxToHwMinFilter in gx_texture.cpp.
|
|
static const uint8_t filterLut[] = {0, 2, 4, 0, 1, 3, 5, 0};
|
|
meta.minFilter = filterLut[(word0 >> 5) & 7];
|
|
meta.magFilter = (word0 >> 4) & 1;
|
|
|
|
// GXInitTexObj sets flags bit 1 for non-CI formats, but GXInitTexObjCI clears it.
|
|
// Accept CI textures even when bit 1 is cleared, as long as the payload is sane.
|
|
bool isValid = (flags & 0x02) != 0;
|
|
if (!isValid) {
|
|
if (IsKnownTexFormat(meta.format) && meta.dataAddr != 0) {
|
|
isValid = true;
|
|
}
|
|
}
|
|
if (!isValid) {
|
|
return TexObjMeta{};
|
|
}
|
|
|
|
} catch (...) {
|
|
// If we can't read the guest memory, return empty meta
|
|
}
|
|
return meta;
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool IsUsableTexObjMeta(const TexObjMeta& meta) {
|
|
return meta.dataAddr != 0 &&
|
|
meta.width != 0 &&
|
|
meta.height != 0 &&
|
|
IsKnownTexFormat(meta.format);
|
|
}
|
|
|
|
bool TexObjIntervalLess(const TexObjIntervalEntry& lhs, const TexObjIntervalEntry& rhs) {
|
|
if (lhs.dataAddr != rhs.dataAddr) {
|
|
return lhs.dataAddr < rhs.dataAddr;
|
|
}
|
|
return lhs.objAddr < rhs.objAddr;
|
|
}
|
|
|
|
bool TryBuildTexObjIntervalEntry(TexObjSlot& slot, TexObjIntervalEntry& outEntry) {
|
|
if (!IsUsableTexObjMeta(slot)) {
|
|
return false;
|
|
}
|
|
|
|
const uint8_t maxLod = slot.maxLod > 0.0f ? static_cast<uint8_t>(std::min(slot.maxLod, 255.0f)) : 0u;
|
|
const uint32_t texSize =
|
|
GXGetTexBufferSize(slot.width, slot.height, slot.format, static_cast<GXBool>(slot.mipmap), maxLod);
|
|
if (texSize == 0) {
|
|
return false;
|
|
}
|
|
|
|
outEntry = TexObjIntervalEntry{
|
|
.dataAddr = CanonicalizeGxMainRamAddress(slot.dataAddr),
|
|
.size = texSize,
|
|
.objAddr = slot.objAddr,
|
|
.slot = &slot,
|
|
};
|
|
return true;
|
|
}
|
|
|
|
// --- Slot lookup -------------------------------------------------------------
|
|
|
|
TexObjSlot* FindTexObjSlot(uint32_t addr) {
|
|
if (g_texObjSlotMemo != nullptr && g_texObjSlotMemoAddr == addr) {
|
|
return g_texObjSlotMemo;
|
|
}
|
|
const auto it = g_TexObjMeta.find(addr);
|
|
if (it == g_TexObjMeta.end()) {
|
|
return nullptr;
|
|
}
|
|
g_texObjSlotMemoAddr = addr;
|
|
g_texObjSlotMemo = &it->second;
|
|
return g_texObjSlotMemo;
|
|
}
|
|
|
|
TexObjSlot& FindOrCreateTexObjSlot(uint32_t addr) {
|
|
if (TexObjSlot* existing = FindTexObjSlot(addr)) {
|
|
return *existing;
|
|
}
|
|
if (g_TexObjMeta.empty()) {
|
|
g_TexObjMeta.reserve(kTexObjTableReserve);
|
|
}
|
|
TexObjSlot& slot = g_TexObjMeta[addr];
|
|
slot.objAddr = addr;
|
|
g_texObjSlotMemoAddr = addr;
|
|
g_texObjSlotMemo = &slot;
|
|
return slot;
|
|
}
|
|
|
|
// --- DCStoreRange interval index --------------------------------------------
|
|
|
|
void RebuildTexObjIntervalsLocked() {
|
|
g_texObjIntervals.clear();
|
|
g_texObjIntervals.reserve(g_TexObjMeta.size());
|
|
g_texObjIntervalMaxSize = 0;
|
|
for (auto& entry : g_TexObjMeta) {
|
|
TexObjSlot& slot = entry.second;
|
|
slot.pendingIntervalUpdate = false;
|
|
TexObjIntervalEntry interval{};
|
|
if (TryBuildTexObjIntervalEntry(slot, interval)) {
|
|
g_texObjIntervalMaxSize = std::max(g_texObjIntervalMaxSize, interval.size);
|
|
g_texObjIntervals.push_back(interval);
|
|
slot.indexedDataAddr = interval.dataAddr;
|
|
slot.indexedSize = interval.size;
|
|
slot.inIntervalIndex = true;
|
|
} else {
|
|
slot.indexedDataAddr = 0;
|
|
slot.indexedSize = 0;
|
|
slot.inIntervalIndex = false;
|
|
}
|
|
}
|
|
|
|
std::sort(g_texObjIntervals.begin(), g_texObjIntervals.end(), TexObjIntervalLess);
|
|
g_texObjIntervalsRebuildAll = false;
|
|
g_texObjIntervalsPending.clear();
|
|
}
|
|
|
|
// Records that the caller is about to mutate the metadata of `slot`. The slot
|
|
// remembers what it currently contributes to the index, so this only has to be
|
|
// an O(1) enqueue with an O(1) dedupe - no linear scan and, critically, no
|
|
// capacity limit that silently degrades into a full rebuild.
|
|
void QueueTexObjIntervalUpdate(TexObjSlot& slot) {
|
|
if (g_texObjIntervalsRebuildAll || slot.pendingIntervalUpdate) {
|
|
return;
|
|
}
|
|
slot.pendingIntervalUpdate = true;
|
|
g_texObjIntervalsPending.push_back(&slot);
|
|
}
|
|
|
|
// Returns true when the sorted index actually had to shift elements, which is
|
|
// the only expensive outcome. Metadata writebacks that leave the interval
|
|
// untouched - the overwhelming majority, since GXLoadTexObj stores its metadata
|
|
// back twice per call - cost one GXGetTexBufferSize and two compares.
|
|
bool PatchTexObjIntervalEntryLocked(TexObjSlot& slot) {
|
|
TexObjIntervalEntry fresh{};
|
|
const bool hasFresh = TryBuildTexObjIntervalEntry(slot, fresh);
|
|
|
|
if (slot.inIntervalIndex && hasFresh && slot.indexedDataAddr == fresh.dataAddr) {
|
|
if (slot.indexedSize == fresh.size) {
|
|
return false;
|
|
}
|
|
// Same sort position, different payload: fix it up in place.
|
|
const auto it =
|
|
std::lower_bound(g_texObjIntervals.begin(), g_texObjIntervals.end(), fresh, TexObjIntervalLess);
|
|
if (it != g_texObjIntervals.end() && it->dataAddr == fresh.dataAddr && it->objAddr == fresh.objAddr) {
|
|
it->size = fresh.size;
|
|
it->slot = &slot;
|
|
slot.indexedSize = fresh.size;
|
|
g_texObjIntervalMaxSize = std::max(g_texObjIntervalMaxSize, fresh.size);
|
|
return false;
|
|
}
|
|
// Index and slot disagree; fall through and rebuild this entry.
|
|
slot.inIntervalIndex = false;
|
|
}
|
|
|
|
bool moved = false;
|
|
if (slot.inIntervalIndex) {
|
|
const TexObjIntervalEntry stale{
|
|
.dataAddr = slot.indexedDataAddr,
|
|
.size = 0,
|
|
.objAddr = slot.objAddr,
|
|
.slot = &slot,
|
|
};
|
|
const auto staleIt =
|
|
std::lower_bound(g_texObjIntervals.begin(), g_texObjIntervals.end(), stale, TexObjIntervalLess);
|
|
if (staleIt != g_texObjIntervals.end() && staleIt->dataAddr == stale.dataAddr &&
|
|
staleIt->objAddr == stale.objAddr) {
|
|
g_texObjIntervals.erase(staleIt);
|
|
moved = true;
|
|
}
|
|
slot.inIntervalIndex = false;
|
|
}
|
|
|
|
slot.indexedDataAddr = 0;
|
|
slot.indexedSize = 0;
|
|
if (!hasFresh) {
|
|
return moved;
|
|
}
|
|
|
|
const auto insertAt =
|
|
std::lower_bound(g_texObjIntervals.begin(), g_texObjIntervals.end(), fresh, TexObjIntervalLess);
|
|
g_texObjIntervals.insert(insertAt, fresh);
|
|
slot.indexedDataAddr = fresh.dataAddr;
|
|
slot.indexedSize = fresh.size;
|
|
slot.inIntervalIndex = true;
|
|
g_texObjIntervalMaxSize = std::max(g_texObjIntervalMaxSize, fresh.size);
|
|
return true;
|
|
}
|
|
|
|
// Each real move memmoves half the index on average, so a burst of genuine
|
|
// relocations (a course load re-initialising thousands of texobjs) is still
|
|
// cheaper to absorb with one rebuild.
|
|
constexpr size_t kMaxTexObjIntervalPatchMoves = 64;
|
|
|
|
void EnsureTexObjIntervalsLocked() {
|
|
if (g_texObjIntervalsRebuildAll) {
|
|
RebuildTexObjIntervalsLocked();
|
|
return;
|
|
}
|
|
if (g_texObjIntervalsPending.empty()) {
|
|
return;
|
|
}
|
|
|
|
size_t moves = 0;
|
|
for (size_t i = 0; i < g_texObjIntervalsPending.size(); ++i) {
|
|
TexObjSlot& slot = *g_texObjIntervalsPending[i];
|
|
slot.pendingIntervalUpdate = false;
|
|
if (PatchTexObjIntervalEntryLocked(slot)) {
|
|
++moves;
|
|
}
|
|
if (moves >= kMaxTexObjIntervalPatchMoves &&
|
|
g_texObjIntervalsPending.size() - (i + 1) > kMaxTexObjIntervalPatchMoves) {
|
|
// RebuildTexObjIntervalsLocked clears every slot's pending flag and
|
|
// the queue itself, including the entries not visited here.
|
|
RebuildTexObjIntervalsLocked();
|
|
return;
|
|
}
|
|
}
|
|
g_texObjIntervalsPending.clear();
|
|
}
|
|
|
|
void EnsureTlutObjBoundsLocked() {
|
|
if (!g_tlutObjBoundsDirty) {
|
|
return;
|
|
}
|
|
|
|
uint64_t boundsStart = std::numeric_limits<uint64_t>::max();
|
|
uint64_t boundsEnd = 0;
|
|
for (const auto& [objAddr, meta] : g_TlutObjMeta) {
|
|
(void)objAddr;
|
|
if (meta.dataAddr == 0 || meta.entries == 0 || !IsKnownTlutFormat(meta.format)) {
|
|
continue;
|
|
}
|
|
const uint64_t tlutStart = CanonicalizeGxMainRamAddress(meta.dataAddr);
|
|
const uint64_t tlutEnd = tlutStart + static_cast<uint64_t>(meta.entries) * 2u;
|
|
boundsStart = std::min(boundsStart, tlutStart);
|
|
boundsEnd = std::max(boundsEnd, tlutEnd);
|
|
}
|
|
if (boundsStart >= boundsEnd) {
|
|
boundsStart = 0;
|
|
boundsEnd = 0;
|
|
}
|
|
|
|
g_tlutObjBoundsStart = boundsStart;
|
|
g_tlutObjBoundsEnd = boundsEnd;
|
|
g_tlutObjBoundsDirty = false;
|
|
}
|
|
|
|
bool TexObjBackingDiffers(const TexObjMeta& lhs, const TexObjMeta& rhs) {
|
|
return CanonicalizeGxMainRamAddress(lhs.dataAddr) != CanonicalizeGxMainRamAddress(rhs.dataAddr) ||
|
|
lhs.width != rhs.width ||
|
|
lhs.height != rhs.height ||
|
|
lhs.format != rhs.format ||
|
|
lhs.mipmap != rhs.mipmap ||
|
|
lhs.maxLod != rhs.maxLod;
|
|
}
|
|
|
|
bool TexObjMetaDiffers(const TexObjMeta& lhs, const TexObjMeta& rhs) {
|
|
return CanonicalizeGxMainRamAddress(lhs.dataAddr) != CanonicalizeGxMainRamAddress(rhs.dataAddr) ||
|
|
lhs.width != rhs.width ||
|
|
lhs.height != rhs.height ||
|
|
lhs.format != rhs.format ||
|
|
lhs.wrapS != rhs.wrapS ||
|
|
lhs.wrapT != rhs.wrapT ||
|
|
lhs.mipmap != rhs.mipmap ||
|
|
lhs.minFilter != rhs.minFilter ||
|
|
lhs.magFilter != rhs.magFilter ||
|
|
lhs.minLod != rhs.minLod ||
|
|
lhs.maxLod != rhs.maxLod ||
|
|
lhs.lodBias != rhs.lodBias ||
|
|
lhs.biasClamp != rhs.biasClamp ||
|
|
lhs.edgeLod != rhs.edgeLod ||
|
|
lhs.maxAniso != rhs.maxAniso ||
|
|
lhs.tlut != rhs.tlut ||
|
|
lhs.userData != rhs.userData;
|
|
}
|
|
|
|
TexObjMeta MergeGuestTexObjMeta(const TexObjMeta& cached, const TexObjMeta& guest) {
|
|
TexObjMeta merged = guest;
|
|
merged.needsUpload = cached.needsUpload;
|
|
if (TexObjBackingDiffers(cached, guest)) {
|
|
merged.needsUpload = true;
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
// Reads the raw 32 guest bytes of a GXTexObj struct. Returns false (and the
|
|
// caller must treat the shadow as absent) when the address is unreadable.
|
|
bool ReadTexObjShadow(uint32_t addr, uint64_t (&out)[4]) noexcept {
|
|
try {
|
|
out[0] = Memory::Read64(addr + 0x00);
|
|
out[1] = Memory::Read64(addr + 0x08);
|
|
out[2] = Memory::Read64(addr + 0x10);
|
|
out[3] = Memory::Read64(addr + 0x18);
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ShadowEquals(const uint64_t (&a)[4], const uint64_t (&b)[4]) noexcept {
|
|
return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3];
|
|
}
|
|
|
|
// Captures the current guest bytes as the slot's shadow so the cache is marked in sync. HLE
|
|
// GXInitTexObj* callers fetch the meta reference before rewriting guest memory, so their capture
|
|
// is intentionally stale; the next lookup just re-decodes once, which is the safe direction.
|
|
void MarkTexObjSyncedLocked(TexObjSlot& slot) noexcept {
|
|
slot.hasGuestShadow = ReadTexObjShadow(slot.objAddr, slot.guestShadow);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool TryGetOrExtractTexObjMeta(uint32_t addr, TexObjMeta& outMeta) {
|
|
TexObjSlot* slot = FindTexObjSlot(addr);
|
|
if (slot != nullptr) {
|
|
const bool cachedUsable = IsUsableTexObjMeta(*slot);
|
|
if (cachedUsable) {
|
|
// Fast path: guest bytes are byte-identical to what the cache was decoded from. The
|
|
// full 32-byte compare (not a generation stamp) is what catches plain guest stores
|
|
// over the struct that never issue a cache op; see TexObjSlot::guestShadow.
|
|
uint64_t shadow[4];
|
|
if (ReadTexObjShadow(addr, shadow) && slot->hasGuestShadow &&
|
|
ShadowEquals(slot->guestShadow, shadow)) {
|
|
outMeta = *slot;
|
|
return true;
|
|
}
|
|
|
|
const TexObjMeta extracted = ExtractTexObjMetaFromGuest(addr);
|
|
if (IsUsableTexObjMeta(extracted) && TexObjMetaDiffers(*slot, extracted)) {
|
|
// The caller writes the merged result back through
|
|
// GetTexObjMeta, which is what re-establishes the sync.
|
|
outMeta = MergeGuestTexObjMeta(*slot, extracted);
|
|
return true;
|
|
}
|
|
MarkTexObjSyncedLocked(*slot);
|
|
outMeta = *slot;
|
|
return true;
|
|
}
|
|
// Cached metadata can be created early (e.g., by LOD/filter calls) before
|
|
// full InitTexObj has populated dimensions/address. Re-extract from guest
|
|
// memory in that case instead of propagating the stale zeroed entry.
|
|
TexObjMeta extracted = ExtractTexObjMetaFromGuest(addr);
|
|
const bool extractedUsable = IsUsableTexObjMeta(extracted);
|
|
if (extractedUsable) {
|
|
QueueTexObjIntervalUpdate(*slot);
|
|
static_cast<TexObjMeta&>(*slot) = extracted;
|
|
MarkTexObjSyncedLocked(*slot);
|
|
outMeta = extracted;
|
|
return true;
|
|
}
|
|
// Keep legacy behavior for callers that still want to inspect invalid meta.
|
|
outMeta = *slot;
|
|
return true;
|
|
}
|
|
// No slot yet: decode the guest struct without creating one, exactly as
|
|
// before. The first GXLoadTexObj writeback is what materialises the slot.
|
|
outMeta = ExtractTexObjMetaFromGuest(addr);
|
|
if (outMeta.width > 0 && outMeta.height > 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Single choke point for texobj metadata writes: callers mutate the returned reference, so this
|
|
// queues the interval-index patch and re-captures the guest shadow before that happens.
|
|
TexObjMeta& GetTexObjMeta(uint32_t addr) {
|
|
TexObjSlot& slot = FindOrCreateTexObjSlot(addr);
|
|
QueueTexObjIntervalUpdate(slot);
|
|
MarkTexObjSyncedLocked(slot);
|
|
return slot;
|
|
}
|
|
|
|
// Same contract as GetTexObjMeta: the caller mutates the returned reference, so
|
|
// the cached TLUT address bounds have to be recomputed before the next flush.
|
|
TlutObjMeta& GetTlutObjMeta(uint32_t addr) {
|
|
g_tlutObjBoundsDirty = true;
|
|
return g_TlutObjMeta[addr];
|
|
}
|
|
|
|
GXTexObj* GetHostTexObj(uint32_t addr) {
|
|
TexObjSlot* slot = FindTexObjSlot(addr);
|
|
if (slot == nullptr || !slot->host || !slot->host->constructed) {
|
|
const CpuContext* cpu = TryGetCpuContext();
|
|
RT_LOGF(RT_TAG_GX,
|
|
"GXTex: invalid GXTexObj @0x%08X (PC=0x%08X, LR=0x%08X, CTR=0x%08X)\n",
|
|
addr, cpu ? cpu->pc : 0u, cpu ? cpu->lr : 0u, cpu ? cpu->ctr : 0u);
|
|
std::fflush(stderr);
|
|
GXTexObj* obj = CreateHostTexObj(addr);
|
|
MarkHostTexObjConstructed(addr);
|
|
return obj;
|
|
}
|
|
return slot->host->PublicPtr();
|
|
}
|
|
|
|
GXTexObj* CreateHostTexObj(uint32_t addr) {
|
|
TexObjSlot& slot = FindOrCreateTexObjSlot(addr);
|
|
// GXInitTexObj/GXInitTexObjCI land here and rewrite the entire guest struct,
|
|
// resetting the LOD/filter words the HLE does not mirror into this cache.
|
|
// The shadow captured before that rewrite mismatches afterwards, so the
|
|
// next lookup takes exactly one guest re-decode and picks those defaults
|
|
// (and the computed mipmap maxLod) up.
|
|
if (!slot.host) {
|
|
slot.host = std::make_unique<HleTexObj>();
|
|
} else {
|
|
slot.host->Destroy();
|
|
}
|
|
slot.host->EnsureStorageLive();
|
|
return slot.host->PublicPtr();
|
|
}
|
|
|
|
void MarkHostTexObjConstructed(uint32_t addr) {
|
|
TexObjSlot* slot = FindTexObjSlot(addr);
|
|
if (slot != nullptr && slot->host) {
|
|
slot->host->constructed = true;
|
|
}
|
|
}
|
|
|
|
GXTexObj* TryGetHostTexObj(uint32_t addr) {
|
|
TexObjSlot* slot = FindTexObjSlot(addr);
|
|
if (slot != nullptr && slot->host && slot->host->constructed) {
|
|
return slot->host->PublicPtr();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void MarkTexObjsDirtyForRange(uint32_t addr, uint32_t size) {
|
|
if (size == 0) {
|
|
return;
|
|
}
|
|
|
|
// Do this even when no GXTexObj currently describes the range. Games
|
|
// commonly fill and flush a newly recycled heap block before constructing
|
|
// its texture object; the flush is the only authoritative indication that
|
|
// an older GPU-only EFB copy at that address is no longer the source.
|
|
InvalidateEfbCopyDestinationsForRange(addr, size);
|
|
|
|
const uint64_t dirtyStart = CanonicalizeGxMainRamAddress(addr);
|
|
const uint64_t dirtyEnd = dirtyStart + size;
|
|
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
|
|
|
EnsureTexObjIntervalsLocked();
|
|
const auto lower = std::lower_bound(
|
|
g_texObjIntervals.begin(), g_texObjIntervals.end(), static_cast<uint32_t>(dirtyStart),
|
|
[](const TexObjIntervalEntry& entry, uint32_t key) { return entry.dataAddr < key; });
|
|
|
|
for (auto it = lower; it != g_texObjIntervals.begin();) {
|
|
--it;
|
|
const uint64_t texStart = it->dataAddr;
|
|
if (texStart + g_texObjIntervalMaxSize <= dirtyStart) {
|
|
// No entry at or left of this start can reach dirtyStart anymore.
|
|
break;
|
|
}
|
|
const uint64_t texEnd = texStart + it->size;
|
|
if (texEnd <= dirtyStart) {
|
|
// This entry falls short, but an earlier-starting, larger one may
|
|
// still overlap: keep walking until the max-size bound proves
|
|
// otherwise.
|
|
continue;
|
|
}
|
|
it->slot->needsUpload = true;
|
|
}
|
|
|
|
for (auto it = lower; it != g_texObjIntervals.end(); ++it) {
|
|
const uint64_t texStart = it->dataAddr;
|
|
if (texStart >= dirtyEnd) {
|
|
break;
|
|
}
|
|
it->slot->needsUpload = true;
|
|
}
|
|
}
|
|
|
|
// Defined in gx_dl.cpp.
|
|
extern "C" void GxNotifyDisplayListMemoryWrite(uint32_t addr, uint32_t size);
|
|
|
|
// Single entry point for DMA-class host writes into guest RAM (DVD reads, DCZeroRange, locked
|
|
// cache stores) that the SDK brackets with DCInvalidateRange rather than a flush, so the normal
|
|
// DC flush/store interceptors never see them. Without this call, GX caches keyed on guest bytes
|
|
// keep serving stale contents and a reloaded track reuses old texture pixels at the same address.
|
|
extern "C" void GxNotifyGuestRamDmaWrite(uint32_t addr, uint32_t size) {
|
|
if (size == 0) {
|
|
return;
|
|
}
|
|
MarkTexObjsDirtyForRange(addr, size);
|
|
MarkTlutObjsDirtyForRange(addr, size);
|
|
GxNotifyDisplayListMemoryWrite(addr, size);
|
|
}
|
|
|
|
GXTlutObj* CreateHostTlutObj(uint32_t addr) {
|
|
auto& slot = g_HostTlutObjMap[addr];
|
|
if (!slot) {
|
|
slot = std::make_unique<HleTlutObj>();
|
|
} else {
|
|
slot->Destroy();
|
|
}
|
|
slot->EnsureStorageLive();
|
|
return slot->PublicPtr();
|
|
}
|
|
|
|
void MarkHostTlutObjConstructed(uint32_t addr) {
|
|
auto it = g_HostTlutObjMap.find(addr);
|
|
if (it != g_HostTlutObjMap.end() && it->second) {
|
|
it->second->constructed = true;
|
|
}
|
|
}
|
|
|
|
GXTlutObj* GetHostTlutObj(uint32_t addr) {
|
|
auto it = g_HostTlutObjMap.find(addr);
|
|
if (it == g_HostTlutObjMap.end() || !it->second || !it->second->constructed) {
|
|
const CpuContext* cpu = TryGetCpuContext();
|
|
RT_LOGF(RT_TAG_GX,
|
|
"GXTex: invalid GXTlutObj @0x%08X (PC=0x%08X, LR=0x%08X, CTR=0x%08X)\n",
|
|
addr, cpu ? cpu->pc : 0u, cpu ? cpu->lr : 0u, cpu ? cpu->ctr : 0u);
|
|
std::fflush(stderr);
|
|
GXTlutObj* obj = CreateHostTlutObj(addr);
|
|
MarkHostTlutObjConstructed(addr);
|
|
return obj;
|
|
}
|
|
return it->second->PublicPtr();
|
|
}
|
|
|
|
void MarkTlutObjsDirtyForRange(uint32_t addr, uint32_t size) {
|
|
if (size == 0) {
|
|
return;
|
|
}
|
|
|
|
const uint64_t dirtyStart = CanonicalizeGxMainRamAddress(addr);
|
|
const uint64_t dirtyEnd = dirtyStart + size;
|
|
std::lock_guard<std::mutex> guard(g_tlutObjMutex);
|
|
EnsureTlutObjBoundsLocked();
|
|
// Most DCStoreRange calls flush small material/vertex buffers that live
|
|
// nowhere near a palette, so reject them without walking the map.
|
|
if (g_tlutObjBoundsStart >= g_tlutObjBoundsEnd ||
|
|
dirtyEnd <= g_tlutObjBoundsStart ||
|
|
dirtyStart >= g_tlutObjBoundsEnd) {
|
|
return;
|
|
}
|
|
for (auto& [objAddr, meta] : g_TlutObjMeta) {
|
|
if (meta.dataAddr == 0 || meta.entries == 0 || !IsKnownTlutFormat(meta.format)) {
|
|
continue;
|
|
}
|
|
|
|
const uint64_t tlutStart = CanonicalizeGxMainRamAddress(meta.dataAddr);
|
|
const uint64_t tlutEnd = tlutStart + static_cast<uint64_t>(meta.entries) * 2u;
|
|
if (dirtyEnd <= tlutStart || dirtyStart >= tlutEnd) {
|
|
continue;
|
|
}
|
|
|
|
meta.dirty = true;
|
|
}
|
|
}
|