TextureCache: stop clamping DXT decoding to bufw either

DecodeDXTBlocks limited its x loop to min(bufw, w), so when w > bufw everything
from bufw to w was simply never written - the destination is sized for w, so
those columns kept whatever the buffer held.

The software sampler doesn't do that. It addresses a block as
(v >> 2) * (texbufw >> 2) + (u >> 2), which for u past bufw runs on into the
next row's blocks, the same way the linear formats run into the next row. So the
two renderers disagreed on the same texture.

Follow the sampler: decode w texels per row and let the block index carry on,
with the range check widened to cover the blocks that reach past the last row's
stride. Shares the SourceExtent helper from the previous commit, counting 4x4
blocks rather than texels.

Rendering-visible where w > bufw for a DXT texture, which is the case that used
to leave stale contents behind.
This commit is contained in:
Henrik Rydgård committed 2026-09-05 11:25:55 -06:00
1 parent ca9d76fef3
commit 2de11efe4e
1 file changed
+34 -24
+34 -24
View File
@@ -1876,28 +1876,51 @@ static inline void ConvertFormatToRGBA8888(GEPaletteFormat format, u32 *dst, con
ConvertFormatToRGBA8888(GETextureFormat(format), dst, src, numPixels);
}
// How much source a decode actually touches. Rows sit `stride` apart, but each row reads `width`
// of them, and the two come from independent GE registers - so when width > stride the last row
// reaches past the end of its own stride. Anything bounding the source has to take both into
// account, or we read past what we checked. Texels for the linear formats, 4x4 blocks for DXT.
// Same shape as the adjustment TextureReplacer::ComputeHash and the GE recorder already make.
static uint32_t SourceExtent(int width, int stride, int rows) {
if (rows <= 0) {
return 0;
}
const uint32_t extra = width > stride ? (uint32_t)(width - stride) : 0;
return (uint32_t)stride * (uint32_t)rows + extra;
}
template <typename DXTBlock, int n>
static TextureAlpha DecodeDXTBlocks(uint8_t *out, int outPitch, uint32_t texaddr, const uint8_t *texptr,
int w, int h, int bufw, bool reverseColors) {
int minw = std::min(bufw, w);
uint32_t *dst = (uint32_t *)out;
int outPitch32 = outPitch / sizeof(uint32_t);
const DXTBlock *src = (const DXTBlock *)texptr;
if (!Memory::IsValidRange(texaddr, ((h + 3) / 4) * (bufw / 4) * sizeof(DXTBlock))) {
ERROR_LOG_REPORT(Log::TexCache, "DXT%d texture extends beyond valid RAM: %08x + %d x %d", n, texaddr, bufw, h);
uint32_t limited = Memory::ClampValidSizeAt(texaddr, (h / 4) * (bufw / 4) * sizeof(DXTBlock));
// This might possibly be 0, but try to decode what we can (might even be how the PSP behaves.)
h = (((int)limited / sizeof(DXTBlock)) / (bufw / 4)) * 4;
// Blocks are laid out in rows of bufw/4, but a row decodes w/4 of them - and when w > bufw
// that runs on into the next row's blocks, which is what the software sampler does too (see
// the DXT cases in Sampler.cpp). So don't stop at bufw, just make sure the check covers it.
const int blocksPerRow = std::max(bufw / 4, 1);
const int blockRows = (h + 3) / 4;
const int rowBlocks = (w + 3) / 4;
const uint32_t neededBlocks = SourceExtent(rowBlocks, blocksPerRow, blockRows);
if (!Memory::IsValidRange(texaddr, neededBlocks * sizeof(DXTBlock))) {
ERROR_LOG_REPORT(Log::TexCache, "DXT%d texture extends beyond valid RAM: %08x + %d x %d (w=%d)", n, texaddr, bufw, h, w);
const uint32_t limited = Memory::ClampValidSizeAt(texaddr, neededBlocks * sizeof(DXTBlock));
// Drop block rows until the last one's blocks fit too. This might land on 0, but try to
// decode what we can (might even be how the PSP behaves.)
const uint32_t availableBlocks = (uint32_t)(limited / sizeof(DXTBlock));
const uint32_t lastRowBlocks = std::max(rowBlocks, blocksPerRow);
h = availableBlocks >= lastRowBlocks ? (int)((availableBlocks - lastRowBlocks) / blocksPerRow + 1) * 4 : 0;
}
u32 alphaSum = 1;
for (int y = 0; y < h; y += 4) {
u32 blockIndex = (y / 4) * (bufw / 4);
u32 blockIndex = (y / 4) * blocksPerRow;
int blockHeight = std::min(h - y, 4);
for (int x = 0; x < minw; x += 4) {
int blockWidth = std::min(minw - x, 4);
for (int x = 0; x < w; x += 4) {
int blockWidth = std::min(w - x, 4);
if constexpr (n == 1)
DecodeDXT1Block(dst + outPitch32 * y + x, (const DXT1Block *)src + blockIndex, outPitch32, blockWidth, blockHeight, &alphaSum);
else if constexpr (n == 3)
@@ -1951,25 +1974,12 @@ static void Expand4To8Bits(u8 *dest, const u8 *src, int srcWidth) {
}
}
// How many source texels a decode actually touches. Rows sit bufw texels apart, but every row
// reads w of them, and w and bufw are independent GE registers - so when w > bufw the last row
// reaches past the end of its own stride. Anything bounding the source has to take both into
// account, or we read past what we checked. Same shape as the adjustment in
// TextureReplacer::ComputeHash and the GE recorder.
static uint32_t TextureSourceTexels(int w, int bufw, int rows) {
if (rows <= 0) {
return 0;
}
const uint32_t extraw = w > bufw ? (uint32_t)(w - bufw) : 0;
return (uint32_t)bufw * (uint32_t)rows + extraw;
}
// Unswizzles a level into tmpTexBuf32_ and hands back a pointer to it. Sized for the texels the
// decode is going to read rather than just bufw per row - UnswizzleFromMem only fills the stride,
// so when w reaches past it the tail is zeroed instead of left as whatever the buffer held.
const u8 *TextureCacheCommon::UnswizzleToTemp(const u8 *texptr, int w, int h, int bufw, int bytesPerPixel) {
const int rows = (h + 7) & ~7;
const uint32_t texels = TextureSourceTexels(w, bufw, rows);
const uint32_t texels = SourceExtent(w, bufw, rows);
tmpTexBuf32_.resize(texels);
if (w > bufw) {
memset(tmpTexBuf32_.data(), 0, texels * sizeof(u32));
@@ -2019,7 +2029,7 @@ TextureAlpha TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, GETex
const uint32_t bytesPerRow = (bpp * bufw) / 8;
// Swizzled textures are read in 8-row blocks, rounding the height up.
const uint32_t rows = swizzled ? ((h + 7) & ~7) : h;
const uint32_t neededBytes = (bpp * TextureSourceTexels(w, bufw, rows) + 7) / 8;
const uint32_t neededBytes = (bpp * SourceExtent(w, bufw, rows) + 7) / 8;
if (bytesPerRow > 0 && !isPPGE && !Memory::IsValidRange(texaddr, neededBytes)) {
ERROR_LOG_REPORT(Log::TexCache, "Texture extends beyond valid RAM: %08x + %d x %d (w=%d)", texaddr, bufw, h, w);
const uint32_t limited = Memory::ClampValidSizeAt(texaddr, neededBytes);