Files
ppsspp/GPU/Software/TexCache.cpp
T
Henrik RydgårdandClaude Opus 5.5 fdb411dd0e SoftGPU: Draw sprites that texture from themselves in the GE's pixel order
A sprite that reads what it draws goes through the texture cache pixel by
pixel: rows left to right, top to bottom, texture lines loaded as samples
first read them, and pixels reaching memory a 16-byte block at a time, a
pixel after the next block starts (four with bilinear filtering). Fitted to
geprobe exp81 and exp204-exp209 on hardware; Split Second's blur passes now
match the PSP.

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

732 lines
23 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 "Common/Log.h"
#include "Core/Config.h"
#include "Core/MemMap.h"
#include "GPU/Common/TextureDecoder.h"
#include "GPU/GPUState.h"
#include "GPU/Software/BinManager.h"
#include "GPU/Software/Rasterizer.h"
#include "GPU/Software/Sampler.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;
// DXT is cached decoded (exp203).
case GE_TFMT_DXT1: case GE_TFMT_DXT3: case GE_TFMT_DXT5: return 32;
default: return 0;
}
}
static uint32_t DXTBlockBytes(uint8_t dxt) {
return dxt == GE_TFMT_DXT1 ? 8 : 16;
}
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.dxt) {
// Two blocks, decoded: the line's 4 x 8 texels as 8888 (as the sampler decodes them).
const uint32_t blockBytes = DXTBlockBytes(src.dxt);
for (int r = 0; r < 8; ++r) {
const int v = band * 8 + r;
const uint32_t addr = src.addr + (uint32_t)(v >> 2) * src.strideBytes + col * blockBytes;
uint32_t texels[4]{};
if (Memory::IsValidRange(addr, blockBytes)) {
alignas(16) u8 block[16];
CopyFrom(src, addr, block, blockBytes);
for (int x = 0; x < 4; ++x) {
switch (src.dxt) {
case GE_TFMT_DXT1: texels[x] = GetDXT1Texel((const DXT1Block *)block, x, v & 3); break;
case GE_TFMT_DXT3: texels[x] = GetDXT3Texel((const DXT3Block *)block, x, v & 3); break;
default: texels[x] = GetDXT5Texel((const DXT5Block *)block, x, v & 3); break;
}
}
}
memcpy(out + r * 16, texels, 16);
}
return;
}
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.dxt) {
const uint32_t blockBytes = DXTBlockBytes(src.dxt);
start = src.addr + (uint32_t)band * 2 * src.strideBytes + col * blockBytes;
end = start + src.strideBytes + blockBytes;
return;
}
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 == src)) {
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;
}
Load(bits, level, col, band, src);
}
// A miss loads from memory, over the least recently used way.
TexCache::Line *TexCache::Load(int bits, int level, int col, int band, const Source &src) {
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);
return victim;
}
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.swizzled = state.samplerID.swizzle;
src.mirror = Memory::DepthMirrorsActive() && Memory::IsDepthTexVRAMAddress(state.texaddr[i]) ? (state.texaddr[i] & 0x00600000) : 0;
const GETextureFormat fmt = state.samplerID.TexFmt();
src.dxt = fmt == GE_TFMT_DXT1 || fmt == GE_TFMT_DXT3 || fmt == GE_TFMT_DXT5 ? (uint8_t)fmt : 0;
uint32_t lo = src.addr, hi;
if (src.dxt) {
src.strideBytes = (state.texbufw[i] / 4) * DXTBlockBytes(src.dxt);
hi = src.addr + (uint32_t)((cfg.h[i] + 7) / 4) * src.strideBytes;
} else {
src.strideBytes = state.texbufw[i] * cfg.bits / 8;
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::Prepare(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;
return true;
}
bool TexCache::Access() {
if (!currentValid_)
return false;
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) {
if (current_.src[0].dxt) {
DecodedImage(state);
return;
}
CopyLevels(state);
for (const Delivered &line : delivered_)
PutLine(line.level, line.col, line.band, line.data);
}
void TexCache::CopyLevels(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);
state.texptr[level] = img.data();
}
}
void TexCache::PutLine(int level, int col, int band, const uint8_t data[128]) {
const Source &cur = current_.src[level];
std::vector<uint8_t> &img = images_[level];
uint32_t start, end;
LineRange(cur, col, band, start, end);
if (cur.swizzled) {
const uint32_t off = start - cur.addr;
if (off + 128 <= img.size())
memcpy(img.data() + off, data, 128);
return;
}
for (int r = 0; r < 8; ++r) {
const uint32_t off = start - cur.addr + r * cur.strideBytes;
if (off + 16 <= img.size())
memcpy(img.data() + off, data + r * 16, 16);
}
}
void TexCache::BeginSerial(Rasterizer::RasterizerState &state) {
CopyLevels(state);
unflushedCount_ = 0;
// A new generation: no line is in the copies yet.
if (++serialGen_ == 0) {
for (auto &set : lines_) {
for (Line &line : set)
line.viewGen = 0;
}
serialGen_ = 1;
}
}
void TexCache::Defer() {
ReplayLog();
delivered_.clear();
}
void TexCache::PushUnflushed(uint32_t addr, const uint8_t old[16]) {
_dbg_assert_(unflushedCount_ < MAX_UNFLUSHED);
Unflushed &u = unflushed_[unflushedCount_++];
u.addr = MaskAddress(addr);
memcpy(u.old, old, 16);
}
void TexCache::RemoveUnflushed(int index) {
if (index < 0 || index >= unflushedCount_)
return;
memmove(unflushed_ + index, unflushed_ + index + 1, sizeof(Unflushed) * (unflushedCount_ - index - 1));
unflushedCount_--;
}
void TexCache::SerialRead(int level, int u, int v) {
const int col = u * current_.bits / 128, band = v >> 3;
Line *line = Find(current_.bits, level, col, band);
if (line) {
line->lru = ++clock_;
if (line->viewGen != serialGen_) {
uint8_t data[128];
Bytes(*line, data);
PutLine(level, col, band, data);
line->viewGen = serialGen_;
}
return;
}
// Loaded now: memory as it is, but for the pixels still in the output block.
const Source &src = current_.src[level];
line = Load(current_.bits, level, col, band, src);
ReadLine(src, col, band, line->data);
uint32_t start, end;
LineRange(src, col, band, start, end);
for (int k = 0; k < unflushedCount_; ++k) {
const Unflushed &u = unflushed_[k];
if (end <= u.addr || start >= u.addr + 16)
continue;
auto patch = [&](uint32_t addr, uint8_t *dst, uint32_t n) {
const uint32_t lo = std::max(addr, u.addr), hi = std::min(addr + n, u.addr + 16);
if (lo < hi)
memcpy(dst + (lo - addr), u.old + (lo - u.addr), hi - lo);
};
if (src.swizzled) {
patch(start, line->data, 128);
} else {
for (int r = 0; r < 8; ++r)
patch(start + r * src.strideBytes, line->data + r * 16, 16);
}
}
line->captured = true;
coherent_ = false;
PutLine(level, col, band, line->data);
line->viewGen = serialGen_;
}
// The cache holds DXT decoded, so a stale line is 8888 texels: the levels are decoded to 8888, the lines laid
// over that, and the state switched to sample 8888.
void TexCache::DecodedImage(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;
const int cols = (std::max(w, (int)state.texbufw[level]) + 3) / 4;
const int bands = (h + 7) / 8;
const uint32_t strideBytes = cols * 16;
std::vector<uint8_t> &img = images_[level];
img.assign(strideBytes * bands * 8 + 16, 0);
uint8_t line[128];
for (int band = 0; band < bands; ++band) {
for (int col = 0; col < cols; ++col) {
ReadLine(cur, col, band, line);
for (int r = 0; r < 8; ++r)
memcpy(img.data() + (band * 8 + r) * strideBytes + col * 16, line + r * 16, 16);
}
}
for (const Delivered &d : delivered_) {
if (d.level != level || d.col >= cols || d.band >= bands)
continue;
for (int r = 0; r < 8; ++r)
memcpy(img.data() + (d.band * 8 + r) * strideBytes + d.col * 16, d.data + r * 16, 16);
}
state.texptr[level] = img.data();
state.texbufw[level] = (uint16_t)(cols * 4);
}
state.samplerID.texfmt = GE_TFMT_8888;
state.samplerID.swizzle = false;
state.samplerID.useStandardBufw = false;
state.samplerID.hasInvalidPtr = false;
state.samplerID.overReadSafe = true;
// No binner: a sampler not compiled yet falls back to C++ (compiling here would flush mid draw).
state.linear = Sampler::GetLinearFunc(state.samplerID, nullptr);
state.nearest = Sampler::GetNearestFunc(state.samplerID, nullptr);
if (g_Config.iTexFiltering == TEX_FILTER_FORCE_LINEAR) {
state.nearest = state.linear;
} else if (g_Config.iTexFiltering == TEX_FILTER_FORCE_NEAREST) {
state.linear = state.nearest;
}
state.linearQuad = Sampler::GetLinearQuadFunc(state.samplerID, state.linear);
}