Files
ppsspp/GPU/Software/TexCache.cpp
T
Henrik RydgårdandClaude Opus 5.5 2b530dfa3b SoftGPU: Simulate the GE's texture cache
Measured on hardware (ppsspp-re geprobe exp177-202, docs/ge-texture-cache.md): 64 lines of 16 bytes x 8
rows in 16 sets of 4 ways, LRU, the set index split between column and band bits by texel size. A line is
tagged by its position in the texture and its mip level only, so another texture read without TEXFLUSH
gets the first one's bytes where their lines coincide; lines load raw bytes on first read and nothing but
TEXFLUSH drops them, not rendering into the texture, block transfers or CPU writes.

TexCache steps every textured primitive through that, in submission order, from its texel footprint. A
line remembers where it came from and its bytes are copied only before that memory changes (drawing, block
transfers, copies, and anything at all when the CPU is about to run). While every line is from the one
texture read since TEXFLUSH and nothing has been copied, nothing read can differ from memory, so primitives
only log what they read, and TEXFLUSH usually drops the log unread. A primitive that reads bytes differing
from memory, or draws over what it reads, is drawn alone from its texture with what it read from the cache
laid over memory. That replaces SelfTextureSnapshot and its "fits in 8 KB" rule.

All the cache experiments match the PSP exactly except exp177, which samples a block transfer's
destination without TEXSYNC. Within a primitive this still loads every line before it draws; the GE loads
them as it rasterizes (serpentine rows, writes reaching memory a 16-byte block late), which Split
Second's blur (the one dump that changes) depends on. About 2% slower in Tekken 6 and God of War.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 12:27:45 +02:00

546 lines
17 KiB
C++

// Copyright (c) 2012- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <cmath>
#include <cstring>
#include "Core/MemMap.h"
#include "GPU/GPUState.h"
#include "GPU/Software/BinManager.h"
#include "GPU/Software/Rasterizer.h"
#include "GPU/Software/TexCache.h"
// VRAM's mirrors are one memory.
static uint32_t MaskAddress(uint32_t addr) {
addr &= 0x3FFFFFFF;
if ((addr & 0x0F000000) == 0x04000000)
addr &= 0x041FFFFF;
return addr;
}
static int TexelBits(GETextureFormat fmt) {
switch (fmt) {
case GE_TFMT_CLUT4: return 4;
case GE_TFMT_CLUT8: return 8;
case GE_TFMT_5650: case GE_TFMT_5551: case GE_TFMT_4444: case GE_TFMT_CLUT16: return 16;
case GE_TFMT_8888: case GE_TFMT_CLUT32: return 32;
default: return 0; // DXT: not simulated.
}
}
TexCache::TexCache() {
Clear();
}
void TexCache::Clear() {
if (used_ != 0) {
for (auto &set : lines_) {
for (Line &line : set)
line.valid = false;
}
memset(keys_, 0, sizeof(keys_));
used_ = 0;
}
uncapturedLo_ = UINT32_MAX;
uncapturedHi_ = 0;
coherent_ = true;
epochStarted_ = false;
configs_.clear();
log_.clear();
loggedSerial_ = 0;
}
// The set split by bits per texel (exp198, exp199, exp202): four bits of column and band, the column's share
// shrinking as texels get smaller, so a set's lines repeat over a roughly square area of texels.
int TexCache::SetIndex(int bits, int col, int band) {
switch (bits) {
case 32: return (col & 7) | ((band & 1) << 3);
case 16:
case 8: return (col & 3) | ((band & 3) << 2);
default: return (col & 1) | ((band & 7) << 1);
}
}
void TexCache::CopyFrom(const Source &src, uint32_t addr, uint8_t *out, uint32_t n) {
if (!src.mirror) {
const uint32_t avail = Memory::ClampValidSizeAt(addr, n);
if (avail)
memcpy(out, Memory::GetPointerUnchecked(addr), avail);
memset(out + avail, 0, n - avail);
return;
}
// The depth layout keeps runs of 32 aligned bytes together.
for (uint32_t pos = 0; pos < n; ) {
const uint32_t a = addr + pos;
const uint32_t run = std::min(32 - (a & 31), n - pos);
const uint32_t from = Memory::DepthMirrored16(0x04000000 | src.mirror | (a & 0x001FFFFF));
if (Memory::IsValidRange(from, run))
memcpy(out + pos, Memory::GetPointerUnchecked(from), run);
else
memset(out + pos, 0, run);
pos += run;
}
}
void TexCache::ReadLine(const Source &src, int col, int band, uint8_t out[128]) {
if (src.swizzled) {
// A swizzle block is the line: 16 bytes x 8 rows, contiguous.
CopyFrom(src, src.addr + (uint32_t)(band * (src.strideBytes / 16) + col) * 128, out, 128);
return;
}
for (int r = 0; r < 8; ++r)
CopyFrom(src, src.addr + (uint32_t)(band * 8 + r) * src.strideBytes + col * 16, out + r * 16, 16);
}
void TexCache::MemoryRange(const Source &src, int col, int band, uint32_t &start, uint32_t &end) {
LineRange(src, col, band, start, end);
if (src.mirror) {
start &= ~0xFFFFu;
end = (end + 0xFFFF) & ~0xFFFFu;
}
}
void TexCache::LineRange(const Source &src, int col, int band, uint32_t &start, uint32_t &end) {
if (src.swizzled) {
start = src.addr + (uint32_t)(band * (src.strideBytes / 16) + col) * 128;
end = start + 128;
} else {
start = src.addr + (uint32_t)band * 8 * src.strideBytes + col * 16;
end = start + 7 * src.strideBytes + 16;
}
}
void TexCache::Bytes(const Line &line, uint8_t out[128]) const {
if (line.captured)
memcpy(out, line.data, 128);
else
ReadLine(line.src, line.col, line.band, out);
}
TexCache::Line *TexCache::Find(int bits, int level, int col, int band) {
const int s = SetIndex(bits, col, band);
const uint32_t key = Key(level, col, band);
for (int w = 0; w < 4; ++w) {
if (keys_[s][w] == key)
return &lines_[s][w];
}
return nullptr;
}
void TexCache::Touch(int bits, int level, int col, int band, const Source &src) {
Line *line = Find(bits, level, col, band);
if (line) {
line->lru = ++clock_;
if (line->captured || line->src.addr != src.addr || line->src.strideBytes != src.strideBytes || line->src.swizzled != src.swizzled) {
Delivered d{ (uint8_t)level, (uint16_t)col, (uint16_t)band };
uint8_t mem[128];
Bytes(*line, d.data);
ReadLine(src, col, band, mem);
if (memcmp(d.data, mem, 128) != 0)
delivered_.push_back(d);
}
return;
}
// A miss loads from memory, over the least recently used way.
const int s = SetIndex(bits, col, band);
Line *set = lines_[s];
int way = 0;
for (int w = 0; w < 4; ++w) {
if (!set[w].valid) {
way = w;
break;
}
if (set[w].lru < set[way].lru)
way = w;
}
Line *victim = &set[way];
keys_[s][way] = Key(level, col, band);
if (!victim->valid)
used_++;
victim->valid = true;
victim->captured = false;
victim->level = (uint8_t)level;
victim->col = (uint16_t)col;
victim->band = (uint16_t)band;
victim->lru = ++clock_;
victim->src = src;
uint32_t start, end;
MemoryRange(src, col, band, start, end);
uncapturedLo_ = std::min(uncapturedLo_, start);
uncapturedHi_ = std::max(uncapturedHi_, end);
}
bool TexCache::Config::SameSources(const Config &o) const {
if (bits != o.bits || level1 < level0 || o.level1 < o.level0)
return false;
// Lines of the levels both read.
for (int i = std::max(level0, o.level0); i <= std::min(level1, o.level1); ++i) {
if (!(src[i] == o.src[i]))
return false;
}
return lo == o.lo && hi == o.hi;
}
bool TexCache::Config::operator==(const Config &o) const {
if (bits != o.bits || level0 != o.level0 || level1 != o.level1 || clampS != o.clampS || clampT != o.clampT || linear != o.linear)
return false;
for (int i = level0; i <= level1; ++i) {
if (w[i] != o.w[i] || h[i] != o.h[i] || !(src[i] == o.src[i]))
return false;
}
return true;
}
void TexCache::BeforeWrite(uint32_t start, uint32_t end) {
// The logged reads, if they're of this memory, have to be in the cache to keep what they loaded.
if (!log_.empty() && end > epoch_.lo && start < epoch_.hi)
ReplayLog();
if (used_ == 0 || end <= uncapturedLo_ || start >= uncapturedHi_)
return;
for (auto &set : lines_) {
for (Line &line : set) {
if (!line.valid || line.captured)
continue;
uint32_t ls, le;
MemoryRange(line.src, line.col, line.band, ls, le);
if (le <= start || ls >= end)
continue;
ReadLine(line.src, line.col, line.band, line.data);
line.captured = true;
coherent_ = false;
}
}
}
void TexCache::CaptureAll() {
ReplayLog();
if (used_ == 0 || uncapturedLo_ >= uncapturedHi_)
return;
for (auto &set : lines_) {
for (Line &line : set) {
if (line.valid && !line.captured) {
ReadLine(line.src, line.col, line.band, line.data);
line.captured = true;
coherent_ = false;
}
}
}
uncapturedLo_ = UINT32_MAX;
uncapturedHi_ = 0;
}
bool TexCache::MakeConfig(const Rasterizer::RasterizerState &state, Config &cfg) {
cfg.bits = TexelBits(state.samplerID.TexFmt());
if (cfg.bits == 0)
return false;
const int maxLevel = state.maxTexLevel;
cfg.level0 = 0;
cfg.level1 = maxLevel;
if (maxLevel != 0 && state.TexLevelMode() == GE_TEXLEVEL_MODE_CONST) {
// A constant level, and the next one when blending between levels.
const int offset = std::max(0, (int)state.texLevelOffset);
cfg.level0 = std::min(offset >> 4, maxLevel);
cfg.level1 = std::min(cfg.level0 + ((offset & 15) && state.mipFilt ? 1 : 0), maxLevel);
}
cfg.clampS = state.samplerID.clampS;
cfg.clampT = state.samplerID.clampT;
cfg.linear = state.minFilt || state.magFilt;
cfg.lo = UINT32_MAX;
cfg.hi = 0;
for (int i = 0; i <= maxLevel; ++i) {
cfg.w[i] = (uint16_t)state.samplerID.cached.sizes[i].w;
cfg.h[i] = (uint16_t)state.samplerID.cached.sizes[i].h;
Source &src = cfg.src[i];
src.addr = MaskAddress(state.texaddr[i]);
src.strideBytes = state.texbufw[i] * cfg.bits / 8;
src.swizzled = state.samplerID.swizzle;
src.mirror = Memory::DepthMirrorsActive() && Memory::IsDepthTexVRAMAddress(state.texaddr[i]) ? (state.texaddr[i] & 0x00600000) : 0;
uint32_t lo = src.addr, hi = src.addr + (uint32_t)((cfg.h[i] + 7) & ~7) * src.strideBytes + (uint32_t)cfg.w[i] * cfg.bits / 8;
if (src.mirror) {
lo &= ~0xFFFFu;
hi = (hi + 0xFFFF) & ~0xFFFFu;
}
cfg.lo = std::min(cfg.lo, lo);
cfg.hi = std::max(cfg.hi, hi);
}
return true;
}
// The vertices' texture coordinates bound what a primitive samples (perspective correct interpolation stays
// within them).
bool TexCache::MakeBounds(const BinItem &item, const Rasterizer::RasterizerState &state, Bounds &b) {
int count;
switch (item.type) {
case BinItemType::TRIANGLE: count = 3; break;
case BinItemType::RECT: case BinItemType::SPRITE: case BinItemType::LINE: count = 2; break;
case BinItemType::POINT: count = 1; break;
default: return false;
}
const VertexData *vs[3] = { &item.v0, &item.v1, &item.v2 };
b.bounded = true;
for (int i = 0; i < count; ++i) {
float s = vs[i]->texturecoords.s(), t = vs[i]->texturecoords.t();
if (state.textureProj) {
const float q = vs[i]->texturecoords.q();
if (!(q > 0.0f)) {
b.bounded = false;
return true;
}
s /= q;
t /= q;
}
if (i == 0) {
b.smin = b.smax = s;
b.tmin = b.tmax = t;
} else {
b.smin = std::min(b.smin, s); b.smax = std::max(b.smax, s);
b.tmin = std::min(b.tmin, t); b.tmax = std::max(b.tmax, t);
}
}
if (state.throughMode) {
// Through mode coordinates are level 0 texels.
const float sw = 1.0f / (float)state.samplerID.cached.sizes[0].w, th = 1.0f / (float)state.samplerID.cached.sizes[0].h;
b.smin *= sw; b.smax *= sw;
b.tmin *= th; b.tmax *= th;
}
if (!(b.smin >= -4096.0f && b.smax <= 4096.0f && b.tmin >= -4096.0f && b.tmax <= 4096.0f))
b.bounded = false;
return true;
}
// Which lines a primitive can read, per level: its coordinate bounds widened by a texel for bilinear
// filtering, then clamped or wrapped as the sampler does.
void TexCache::MakeFootprint(const Config &cfg, const Bounds &b, std::vector<Footprint> &out) {
out.clear();
// The texels the samples can reach, as segments within the texture after clamping or wrapping. The far
// coordinate is exclusive: samples are at pixel centers inside the primitive.
struct Segment {
int first, last;
};
auto segments = [&](float lo, float hi, int size, bool clamp, Segment seg[2]) -> int {
const int texels = std::min(size, 512);
int first, last;
if (!b.bounded) {
first = 0;
last = texels - 1;
} else {
lo *= size;
hi *= size;
if (cfg.linear) {
first = (int)floorf(lo - 0.5f);
last = std::max(first + 1, (int)ceilf(hi - 0.5f));
} else {
first = (int)floorf(lo);
last = std::max(first, (int)ceilf(hi) - 1);
}
}
if (clamp) {
seg[0] = Segment{ std::clamp(first, 0, texels - 1), std::clamp(last, 0, texels - 1) };
return 1;
}
if (last - first + 1 >= texels) {
seg[0] = Segment{ 0, texels - 1 };
return 1;
}
first &= texels - 1;
last &= texels - 1;
if (first <= last) {
seg[0] = Segment{ first, last };
return 1;
}
seg[0] = Segment{ first, texels - 1 };
seg[1] = Segment{ 0, last };
return 2;
};
for (int level = cfg.level0; level <= cfg.level1; ++level) {
Segment us[2], vs[2];
const int nu = segments(b.smin, b.smax, cfg.w[level], cfg.clampS, us);
const int nv = segments(b.tmin, b.tmax, cfg.h[level], cfg.clampT, vs);
for (int j = 0; j < nv; ++j) {
Footprint f{ level, nu, {}, {}, vs[j].first >> 3, vs[j].last >> 3 };
for (int i = 0; i < nu; ++i) {
f.col0[i] = us[i].first * cfg.bits / 128;
f.col1[i] = us[i].last * cfg.bits / 128;
}
out.push_back(f);
}
}
}
// In raster order: band by band, and along each band left to right (across a wrap too).
void TexCache::Step(const Config &cfg, const std::vector<Footprint> &fp) {
for (const Footprint &f : fp) {
for (int band = f.band0; band <= f.band1; ++band) {
for (int s = 0; s < f.segments; ++s) {
for (int col = f.col0[s]; col <= f.col1[s]; ++col)
Touch(cfg.bits, f.level, col, band, cfg.src[f.level]);
}
}
}
}
void TexCache::ReplayLog() {
if (log_.empty())
return;
std::vector<Footprint> fp;
for (const Logged &l : log_) {
const Config &cfg = configs_[l.config];
MakeFootprint(cfg, l.bounds, fp);
Step(cfg, fp);
}
// Coherent: nothing logged could read anything stale.
delivered_.clear();
log_.clear();
configs_.clear();
loggedSerial_ = 0;
}
bool TexCache::Access(const BinItem &item, const Rasterizer::RasterizerState &state) {
delivered_.clear();
footprintValid_ = false;
currentValid_ = false;
if (!state.enableTextures)
return false;
if (state.serial != configSerial_ || state.serial == 0) {
configSerial_ = state.serial;
configOk_ = MakeConfig(state, current_);
}
if (!configOk_ || !MakeBounds(item, state, bounds_))
return false;
currentValid_ = true;
if (coherent_) {
if (!epochStarted_) {
epoch_ = current_;
epochStarted_ = true;
epochSerial_ = 0;
}
if (epochSerial_ != configSerial_ || configSerial_ == 0) {
sameEpoch_ = current_.SameSources(epoch_);
epochSerial_ = configSerial_;
}
if (sameEpoch_) {
if (configs_.empty() || loggedSerial_ != configSerial_ || configSerial_ == 0) {
if (configs_.size() >= 0xFFFF)
ReplayLog();
configs_.push_back(current_);
loggedSerial_ = configSerial_;
}
log_.push_back(Logged{ (uint16_t)(configs_.size() - 1), bounds_ });
if (log_.size() >= 4096)
ReplayLog();
return false;
}
// Another texture: from now on hits can give the first one's bytes.
ReplayLog();
coherent_ = false;
}
MakeFootprint(current_, bounds_, footprint_);
footprintValid_ = true;
Step(current_, footprint_);
return !delivered_.empty();
}
bool TexCache::FootprintReads(uint32_t base, int bpp, int stride, int x1, int y1, int x2, int y2) {
const uint32_t strideBytes = (uint32_t)stride * bpp;
if (strideBytes == 0 || !currentValid_)
return false;
const uint32_t rectStart = base + (uint32_t)(y1 * stride + x1) * bpp;
const uint32_t rectEnd = base + (uint32_t)(y2 * stride + x2 + 1) * bpp;
if (rectEnd <= current_.lo || rectStart >= current_.hi)
return false;
if (!footprintValid_) {
MakeFootprint(current_, bounds_, footprint_);
footprintValid_ = true;
}
// Whether bytes [a, a + n) are inside the rectangle's rows and columns.
auto inRect = [&](uint32_t a, uint32_t n) {
if (a + n <= rectStart || a >= rectEnd)
return false;
const uint32_t off = a - base;
const int row = (int)(off / strideBytes);
const uint32_t col0 = off % strideBytes, col1 = col0 + n - 1;
if (row < y1 || row > y2)
return col1 >= strideBytes; // Runs into the next row.
return col1 >= (uint32_t)x1 * bpp && col0 < (uint32_t)(x2 + 1) * bpp;
};
for (const Footprint &f : footprint_) {
const Source &src = current_.src[f.level];
for (int s = 0; s < f.segments; ++s) {
// The whole footprint's span first; then line by line, row by row.
uint32_t s0, e0, s1, e1;
MemoryRange(src, f.col0[s], f.band0, s0, e0);
MemoryRange(src, f.col1[s], f.band1, s1, e1);
if (std::max(e0, e1) <= rectStart || std::min(s0, s1) >= rectEnd)
continue;
// Through a depth mirror, any byte of the pages may be one it reads.
if (src.mirror)
return true;
for (int band = f.band0; band <= f.band1; ++band) {
for (int col = f.col0[s]; col <= f.col1[s]; ++col) {
uint32_t ls, le;
LineRange(src, col, band, ls, le);
if (le <= rectStart || ls >= rectEnd)
continue;
if (src.swizzled) {
if (inRect(ls, 128))
return true;
continue;
}
for (int r = 0; r < 8; ++r) {
if (inRect(ls + r * src.strideBytes, 16))
return true;
}
}
}
}
}
return false;
}
void TexCache::Image(Rasterizer::RasterizerState &state) {
for (int level = 0; level <= state.maxTexLevel; ++level) {
const Source &cur = current_.src[level];
const int w = state.samplerID.cached.sizes[level].w;
const int h = state.samplerID.cached.sizes[level].h;
// What the sampler can address: whole bands, and a row's texels past the stride.
const uint32_t rows = (uint32_t)((h + 7) & ~7);
const uint32_t bytes = rows * cur.strideBytes + (uint32_t)std::max(0, w * current_.bits / 8 - (int)cur.strideBytes) + 16;
std::vector<uint8_t> &img = images_[level];
img.resize(bytes);
CopyFrom(cur, cur.addr, img.data(), bytes);
for (const Delivered &line : delivered_) {
if (line.level != level)
continue;
uint32_t start, end;
LineRange(cur, line.col, line.band, start, end);
if (cur.swizzled) {
const uint32_t off = start - cur.addr;
if (off + 128 <= bytes)
memcpy(img.data() + off, line.data, 128);
} else {
for (int r = 0; r < 8; ++r) {
const uint32_t off = start - cur.addr + r * cur.strideBytes;
if (off + 16 <= bytes)
memcpy(img.data() + off, line.data + r * 16, 16);
}
}
}
state.texptr[level] = img.data();
}
}