From a4b4489d30b62634ea1fd15f4b77e03ccd46f0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 16 Jul 2026 18:52:17 +0200 Subject: [PATCH 1/7] Move code around --- GPU/Common/TextureCacheCommon.cpp | 276 +++++++++++++++--------------- 1 file changed, 138 insertions(+), 138 deletions(-) diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index fe59aa1727..0be511869e 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -386,6 +386,39 @@ SamplerCacheKey GetFramebufferSamplingParams(const GEState &gstate, u16 bufferWi return key; } +static u32 ComputeTextureHash(TextureReplacer &replacer, u32 addr, int bufw, int w, int h, bool swizzled, const TexCacheEntry *entry) { + const GETextureFormat format = entry->format; + if (replacer.Enabled()) { + return replacer.ComputeHash(addr, bufw, w, h, swizzled, format, entry->maxSeenV); + } + + if (h == 512 && entry->maxSeenV < 512 && entry->maxSeenV != 0) { + h = (int)entry->maxSeenV; + } + + u32 sizeInRAM; + if (swizzled) { + // In swizzle mode, textures are stored in rectangular blocks with the height 8. + // That means that for a 64x4 texture, like in issue #9308, we would only hash half of the texture! + // In theory, we should make sure to only hash half of each block, but in reality it's not likely that + // games are using that memory for anything else. So we'll just make sure to compute the full size to hash. + // To do that, we just use the same calculation but round the height upwards to the nearest multiple of 8. + sizeInRAM = (textureBitsPerPixel[format] * bufw * ((h + 7) & ~7)) >> 3; + } else { + sizeInRAM = (textureBitsPerPixel[format] * bufw * h) >> 3; + } + const u32 *checkp = (const u32 *)Memory::GetPointer(addr); + + if (Memory::IsValidAddress(addr + sizeInRAM)) { + gpuStats.perFrame.numTextureDataBytesHashed += sizeInRAM; + + // return XXH64(checkp, sizeInRAM, 0xBACD7814); + return StableQuickTexHash(checkp, sizeInRAM); + } else { + return 0; + } +} + // NOTE: this is overridden in TextureCacheGLES, with some extra color conversion. void TextureCacheCommon::UpdateCurrentClut(GEPaletteFormat clutFormat, u32 clutBase, bool clutIndexIsSimple) { const u32 clutBaseBytes = clutFormat == GE_CMODE_32BIT_ABGR8888 ? (clutBase * sizeof(u32)) : (clutBase * sizeof(u16)); @@ -761,6 +794,111 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { return entry; } +TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { + SetTexture(); + + TextureApplyResult result; + TexCacheEntry *entry = nextTexture_; + if (!entry) { + // Maybe we bound a framebuffer? + ForgetLastTexture(); + if (failedTexture_) { + // Backends should handle this by binding a black texture with 0 alpha. + BindTexture(nullptr); + } else if (nextFramebufferTexture_) { + // ApplyTextureFramebuffer is responsible for setting SetTextureFullAlpha. + ApplyTextureFramebuffer(nextFramebufferTexture_, gstate.getTextureFormat(), nextFramebufferTextureChannel_); + result.framebuffer = nextFramebufferTexture_; + nextFramebufferTexture_ = nullptr; + } + // We don't set the 3D texture state here or anything else, on some backends (?) + // a nextTexture_ of nullptr means keep the current texture. + return result; + } + + nextTexture_ = nullptr; + + UpdateMaxSeenV(entry, gstate.isModeThrough()); + + if (nextNeedsRebuild_) { + // Regardless of hash fails or otherwise, if this is a video, mark it frequently changing. + // This prevents temporary scaling perf hits on the first second of video. + if (IsVideo(entry->addr)) { + entry->status |= TexStatus::VIDEO; + } else { + entry->status &= ~TexStatus::VIDEO; + } + + if (nextNeedsRehash_) { + PROFILE_THIS_SCOPE("texhash"); + // Update the hash on the texture. + int w = gstate.getTextureWidth(0); + int h = gstate.getTextureHeight(0); + const bool swizzled = gstate.isTextureSwizzled(); + entry->fullhash = ComputeTextureHash(replacer_, entry->addr, entry->bufw, w, h, swizzled, entry); + + // TODO: Here we could check the secondary cache; maybe the texture is in there? + // We would need to abort the build if so. + } + if (nextNeedsChange_) { + // This texture existed previously, let's handle the change. + HandleTextureChange(entry, nextChangeReason_, false, true); + } + // We actually build afterward (shared with rehash rebuild.) + } else if (nextNeedsRehash_) { + // Okay, this matched and didn't change - but let's check the hash. Maybe it will change. + bool doDelete = true; + if (!CheckFullHash(entry, doDelete)) { + HandleTextureChange(entry, "hash fail", true, doDelete); + nextNeedsRebuild_ = true; + } else if (nextTexture_ != nullptr) { + // The secondary cache may choose an entry from its storage by setting nextTexture_. + // This means we should set that, instead of our previous entry. + entry = nextTexture_; + nextTexture_ = nullptr; + UpdateMaxSeenV(entry, gstate.isModeThrough()); + } + } + + // Okay, now actually rebuild the texture if needed. + if (nextNeedsRebuild_) { + _assert_(!entry->texturePtr); + BuildTexture(entry); + ForgetLastTexture(); + } + + gstate_c.SetTextureIsVideo((entry->status & TexStatus::VIDEO) != 0); + entry->lastFrame = gpuStats.totals.numFlips; + if (entry->status & TexStatus::CLUT_GPU) { + _dbg_assert_(entry->status & TexStatus::CLUT8_INDEXED); + // Special process. + if (doBind) { + ApplyTextureDepalFramebufferCLUT(entry); + } + gstate_c.SetTextureSolidAlpha(false); + gstate_c.SetTextureIs3D(false); + gstate_c.SetTextureIsArray(false); + return TextureApplyResult{}; + } else { + if (doBind) { + BindTexture(entry); + } + gstate_c.SetTextureSolidAlpha((entry->status & TexStatus::ALPHA_SOLID) != 0); + gstate_c.SetTextureIs3D((entry->status & TexStatus::IS_3D) != 0); + gstate_c.SetTextureIsArray(false); + if (entry->status & TexStatus::CLUT8_INDEXED) { + bool smoothedDepal = false; + u32 depthUpperBits = 0; + ClutTexture clutTexture = clutTextureCache_.GetClutTexture(gstate.getClutPaletteFormat(), clutHash_, clutBuf_); + BindAsClutTexture(clutTexture.texture, false); + gstate_c.SetShaderDepal(ShaderDepalMode::NORMAL, GE_FORMAT_CLUT8); + } else { + gstate_c.SetShaderDepal(ShaderDepalMode::OFF); + } + } + return TextureApplyResult{entry, nullptr}; +} + static bool GetBestFramebufferCandidate(FramebufferManagerCommon *fbManager, const TextureDefinition &entry, u32 texAddrOffset, AttachCandidate *bestCandidate, const char *context) { gpuStats.perFrame.numFramebufferEvaluations++; @@ -2178,144 +2316,6 @@ TextureAlpha TextureCacheCommon::ReadIndexedTex(u8 *out, int outPitch, int level } } -static u32 ComputeTextureHash(TextureReplacer &replacer, u32 addr, int bufw, int w, int h, bool swizzled, const TexCacheEntry *entry) { - const GETextureFormat format = entry->format; - if (replacer.Enabled()) { - return replacer.ComputeHash(addr, bufw, w, h, swizzled, format, entry->maxSeenV); - } - - if (h == 512 && entry->maxSeenV < 512 && entry->maxSeenV != 0) { - h = (int)entry->maxSeenV; - } - - u32 sizeInRAM; - if (swizzled) { - // In swizzle mode, textures are stored in rectangular blocks with the height 8. - // That means that for a 64x4 texture, like in issue #9308, we would only hash half of the texture! - // In theory, we should make sure to only hash half of each block, but in reality it's not likely that - // games are using that memory for anything else. So we'll just make sure to compute the full size to hash. - // To do that, we just use the same calculation but round the height upwards to the nearest multiple of 8. - sizeInRAM = (textureBitsPerPixel[format] * bufw * ((h + 7) & ~7)) >> 3; - } else { - sizeInRAM = (textureBitsPerPixel[format] * bufw * h) >> 3; - } - const u32 *checkp = (const u32 *)Memory::GetPointer(addr); - - if (Memory::IsValidAddress(addr + sizeInRAM)) { - gpuStats.perFrame.numTextureDataBytesHashed += sizeInRAM; - - // return XXH64(checkp, sizeInRAM, 0xBACD7814); - return StableQuickTexHash(checkp, sizeInRAM); - } else { - return 0; - } -} - -TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { - SetTexture(); - - TextureApplyResult result; - TexCacheEntry *entry = nextTexture_; - if (!entry) { - // Maybe we bound a framebuffer? - ForgetLastTexture(); - if (failedTexture_) { - // Backends should handle this by binding a black texture with 0 alpha. - BindTexture(nullptr); - } else if (nextFramebufferTexture_) { - // ApplyTextureFramebuffer is responsible for setting SetTextureFullAlpha. - ApplyTextureFramebuffer(nextFramebufferTexture_, gstate.getTextureFormat(), nextFramebufferTextureChannel_); - result.framebuffer = nextFramebufferTexture_; - nextFramebufferTexture_ = nullptr; - } - // We don't set the 3D texture state here or anything else, on some backends (?) - // a nextTexture_ of nullptr means keep the current texture. - return result; - } - - nextTexture_ = nullptr; - - UpdateMaxSeenV(entry, gstate.isModeThrough()); - - if (nextNeedsRebuild_) { - // Regardless of hash fails or otherwise, if this is a video, mark it frequently changing. - // This prevents temporary scaling perf hits on the first second of video. - if (IsVideo(entry->addr)) { - entry->status |= TexStatus::VIDEO; - } else { - entry->status &= ~TexStatus::VIDEO; - } - - if (nextNeedsRehash_) { - PROFILE_THIS_SCOPE("texhash"); - // Update the hash on the texture. - int w = gstate.getTextureWidth(0); - int h = gstate.getTextureHeight(0); - const bool swizzled = gstate.isTextureSwizzled(); - entry->fullhash = ComputeTextureHash(replacer_, entry->addr, entry->bufw, w, h, swizzled, entry); - - // TODO: Here we could check the secondary cache; maybe the texture is in there? - // We would need to abort the build if so. - } - if (nextNeedsChange_) { - // This texture existed previously, let's handle the change. - HandleTextureChange(entry, nextChangeReason_, false, true); - } - // We actually build afterward (shared with rehash rebuild.) - } else if (nextNeedsRehash_) { - // Okay, this matched and didn't change - but let's check the hash. Maybe it will change. - bool doDelete = true; - if (!CheckFullHash(entry, doDelete)) { - HandleTextureChange(entry, "hash fail", true, doDelete); - nextNeedsRebuild_ = true; - } else if (nextTexture_ != nullptr) { - // The secondary cache may choose an entry from its storage by setting nextTexture_. - // This means we should set that, instead of our previous entry. - entry = nextTexture_; - nextTexture_ = nullptr; - UpdateMaxSeenV(entry, gstate.isModeThrough()); - } - } - - // Okay, now actually rebuild the texture if needed. - if (nextNeedsRebuild_) { - _assert_(!entry->texturePtr); - BuildTexture(entry); - ForgetLastTexture(); - } - - gstate_c.SetTextureIsVideo((entry->status & TexStatus::VIDEO) != 0); - entry->lastFrame = gpuStats.totals.numFlips; - if (entry->status & TexStatus::CLUT_GPU) { - _dbg_assert_(entry->status & TexStatus::CLUT8_INDEXED); - // Special process. - if (doBind) { - ApplyTextureDepalFramebufferCLUT(entry); - } - gstate_c.SetTextureSolidAlpha(false); - gstate_c.SetTextureIs3D(false); - gstate_c.SetTextureIsArray(false); - return TextureApplyResult{}; - } else { - if (doBind) { - BindTexture(entry); - } - gstate_c.SetTextureSolidAlpha((entry->status & TexStatus::ALPHA_SOLID) != 0); - gstate_c.SetTextureIs3D((entry->status & TexStatus::IS_3D) != 0); - gstate_c.SetTextureIsArray(false); - if (entry->status & TexStatus::CLUT8_INDEXED) { - bool smoothedDepal = false; - u32 depthUpperBits = 0; - ClutTexture clutTexture = clutTextureCache_.GetClutTexture(gstate.getClutPaletteFormat(), clutHash_, clutBuf_); - BindAsClutTexture(clutTexture.texture, false); - gstate_c.SetShaderDepal(ShaderDepalMode::NORMAL, GE_FORMAT_CLUT8); - } else { - gstate_c.SetShaderDepal(ShaderDepalMode::OFF); - } - } - return TextureApplyResult{entry, nullptr}; -} - void TextureCacheCommon::ApplySampler(const TextureApplyResult &result, bool flatZ, bool pixelMapped) { SamplerCacheKey samplerKey; if (result.texCacheEntry) { From 0528646ea34dd97b1768e80a875d582d3db066fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 16 Jul 2026 19:07:46 +0200 Subject: [PATCH 2/7] Invert the relationship between the two functions --- GPU/Common/TextureCacheCommon.cpp | 14 ++++++-------- GPU/Common/TextureCacheCommon.h | 3 ++- GPU/D3D11/TextureCacheD3D11.cpp | 14 +++++++------- GPU/GLES/TextureCacheGLES.cpp | 8 ++++---- GPU/Vulkan/TextureCacheVulkan.cpp | 7 +++---- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 0be511869e..47ca3fdd49 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -508,7 +508,7 @@ void TextureCacheCommon::UpdateMaxSeenV(TexCacheEntry *entry, bool throughMode) } } -TexCacheEntry *TextureCacheCommon::SetTexture() { +TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { u8 level = 0; if (IsFakeMipmapChange()) { level = std::max(0, gstate.getTexLevelOffset16() / 16); @@ -522,7 +522,7 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { gstate_c.SetTextureIsArray(false); gstate_c.SetTextureIsFramebuffer(false); gstate_c.SetShaderDepal(ShaderDepalMode::OFF); - return nullptr; + return ApplyTextureFinish(doBind); } const u16 dim = gstate.getTextureDimension(level); @@ -688,7 +688,7 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { nextNeedsRebuild_ = false; failedTexture_ = false; VERBOSE_LOG(Log::G3D, "Texture at %08x found in cache, applying", texaddr); - return entry; //Done! + return ApplyTextureFinish(doBind); //Done! } else { // Wasn't a match, we will rebuild. nextChangeReason_ = reason; @@ -717,7 +717,7 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { nextNeedsRebuild_ = false; SetTextureFramebuffer(bestCandidate); // sets curTexture3D - return nullptr; + return ApplyTextureFinish(doBind); } // Didn't match a framebuffer, keep going. @@ -791,12 +791,10 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { nextFramebufferTexture_ = nullptr; nextNeedsRehash_ = true; nextNeedsRebuild_ = true; - return entry; + return ApplyTextureFinish(doBind); } -TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { - SetTexture(); - +TextureApplyResult TextureCacheCommon::ApplyTextureFinish(bool doBind) { TextureApplyResult result; TexCacheEntry *entry = nextTexture_; if (!entry) { diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index 598ef41b68..6b0120d93b 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -329,6 +329,7 @@ struct TextureApplyResult { // At most one of these will be set. TexCacheEntry *texCacheEntry = nullptr; VirtualFramebuffer *framebuffer = nullptr; + bool otherTexture = false; }; class TextureCacheCommon { @@ -405,7 +406,7 @@ protected: // This updates nextTexture_ / nextFramebufferTexture_, which is then used by ApplyTexture. // TODO: Return stuff directly instead of keeping state. - TexCacheEntry *SetTexture(); + TextureApplyResult ApplyTextureFinish(bool doBind); virtual void BindTexture(TexCacheEntry *entry) = 0; virtual void Unbind() = 0; virtual void ReleaseTexture(TexCacheEntry *entry, bool delete_them) = 0; diff --git a/GPU/D3D11/TextureCacheD3D11.cpp b/GPU/D3D11/TextureCacheD3D11.cpp index cd16a793ed..abc32eddf2 100644 --- a/GPU/D3D11/TextureCacheD3D11.cpp +++ b/GPU/D3D11/TextureCacheD3D11.cpp @@ -444,21 +444,21 @@ DXGI_FORMAT TextureCacheD3D11::GetDestFormat(GETextureFormat format, GEPaletteFo } bool TextureCacheD3D11::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level, bool *isFramebuffer) { - SetTexture(); - if (!nextTexture_) { - return GetCurrentFramebufferTextureDebug(buffer, isFramebuffer); - } - // Apply texture may need to rebuild the texture if we're about to render, or bind a framebuffer. TextureApplyResult textureResult = ApplyTexture(true); - TexCacheEntry *entry = textureResult.texCacheEntry; + if (textureResult.framebuffer) { + return GetCurrentFramebufferTextureDebug(buffer, isFramebuffer); + } + const TexCacheEntry *entry = textureResult.texCacheEntry; if (!entry) { return false; } ID3D11Texture2D *texture = (ID3D11Texture2D *)entry->texturePtr; - if (!texture) + if (!texture) { + // Hm. return false; + } D3D11_TEXTURE2D_DESC desc; texture->GetDesc(&desc); diff --git a/GPU/GLES/TextureCacheGLES.cpp b/GPU/GLES/TextureCacheGLES.cpp index b37d930ec7..a6a16dfa94 100644 --- a/GPU/GLES/TextureCacheGLES.cpp +++ b/GPU/GLES/TextureCacheGLES.cpp @@ -361,15 +361,15 @@ Draw::DataFormat TextureCacheGLES::GetDestFormat(GETextureFormat format, GEPalet bool TextureCacheGLES::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level, bool *isFramebuffer) { ForgetLastTexture(); - SetTexture(); - if (!nextTexture_) { - return GetCurrentFramebufferTextureDebug(buffer, isFramebuffer); - } // Apply texture may need to rebuild the texture if we're about to render, or bind a framebuffer. // We might need a render pass to set the sampling params, unfortunately. Otherwise BuildTexture may crash. framebufferManagerGL_->RebindFramebuffer("RebindFramebuffer - GetCurrentTextureDebug"); TextureApplyResult textureResult = ApplyTexture(false); + if (textureResult.framebuffer) { + return GetCurrentFramebufferTextureDebug(buffer, isFramebuffer); + } + TexCacheEntry *entry = textureResult.texCacheEntry; if (!entry) { return false; diff --git a/GPU/Vulkan/TextureCacheVulkan.cpp b/GPU/Vulkan/TextureCacheVulkan.cpp index 5822d2b824..6998358f01 100644 --- a/GPU/Vulkan/TextureCacheVulkan.cpp +++ b/GPU/Vulkan/TextureCacheVulkan.cpp @@ -1002,13 +1002,12 @@ void TextureCacheVulkan::BoundFramebufferTexture() { } bool TextureCacheVulkan::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level, bool *isFramebuffer) { - SetTexture(); - if (!nextTexture_) { + // Apply texture may need to rebuild the texture if we're about to render, or bind a framebuffer. + TextureApplyResult textureResult = ApplyTexture(true); + if (textureResult.framebuffer) { return GetCurrentFramebufferTextureDebug(buffer, isFramebuffer); } - // Apply texture may need to rebuild the texture if we're about to render, or bind a framebuffer. - TextureApplyResult textureResult = ApplyTexture(true); TexCacheEntry *entry = textureResult.texCacheEntry; if (!entry || !entry->vkTex) { return false; From 130fad68766335af9961b3c0f5ad8ac7269e660e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 16 Jul 2026 20:15:51 +0200 Subject: [PATCH 3/7] Split out a function to finish up binding of framebuffers --- GPU/Common/TextureCacheCommon.cpp | 44 ++++++++++++++++--------------- GPU/Common/TextureCacheCommon.h | 3 ++- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 47ca3fdd49..56603fe938 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -516,13 +516,14 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { u32 texaddr = gstate.getTextureAddress(level); if (!Memory::IsValidAddress(texaddr)) { // Bind a null texture and return. + nextTexture_ = nullptr; Unbind(); gstate_c.SetTextureIsVideo(false); gstate_c.SetTextureIs3D(false); gstate_c.SetTextureIsArray(false); gstate_c.SetTextureIsFramebuffer(false); gstate_c.SetShaderDepal(ShaderDepalMode::OFF); - return ApplyTextureFinish(doBind); + return TextureApplyResult(); } const u16 dim = gstate.getTextureDimension(level); @@ -688,7 +689,7 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { nextNeedsRebuild_ = false; failedTexture_ = false; VERBOSE_LOG(Log::G3D, "Texture at %08x found in cache, applying", texaddr); - return ApplyTextureFinish(doBind); //Done! + return ApplyTextureFinish(entry, doBind); //Done! } else { // Wasn't a match, we will rebuild. nextChangeReason_ = reason; @@ -717,7 +718,7 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { nextNeedsRebuild_ = false; SetTextureFramebuffer(bestCandidate); // sets curTexture3D - return ApplyTextureFinish(doBind); + return ApplyTextureFinishFramebuffer(nextFramebufferTexture_, doBind); } // Didn't match a framebuffer, keep going. @@ -791,29 +792,30 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { nextFramebufferTexture_ = nullptr; nextNeedsRehash_ = true; nextNeedsRebuild_ = true; - return ApplyTextureFinish(doBind); + return ApplyTextureFinish(entry, doBind); } -TextureApplyResult TextureCacheCommon::ApplyTextureFinish(bool doBind) { +TextureApplyResult TextureCacheCommon::ApplyTextureFinishFramebuffer(VirtualFramebuffer *framebuffer, bool doBind) { TextureApplyResult result; - TexCacheEntry *entry = nextTexture_; - if (!entry) { - // Maybe we bound a framebuffer? - ForgetLastTexture(); - if (failedTexture_) { - // Backends should handle this by binding a black texture with 0 alpha. - BindTexture(nullptr); - } else if (nextFramebufferTexture_) { - // ApplyTextureFramebuffer is responsible for setting SetTextureFullAlpha. - ApplyTextureFramebuffer(nextFramebufferTexture_, gstate.getTextureFormat(), nextFramebufferTextureChannel_); - result.framebuffer = nextFramebufferTexture_; - nextFramebufferTexture_ = nullptr; - } - // We don't set the 3D texture state here or anything else, on some backends (?) - // a nextTexture_ of nullptr means keep the current texture. - return result; + // Maybe we bound a framebuffer? + ForgetLastTexture(); + if (failedTexture_) { + // Backends should handle this by binding a black texture with 0 alpha. + BindTexture(nullptr); + } else if (framebuffer) { + // ApplyTextureFramebuffer is responsible for setting SetTextureFullAlpha. + ApplyTextureFramebuffer(framebuffer, gstate.getTextureFormat(), nextFramebufferTextureChannel_); + result.framebuffer = framebuffer; + framebuffer = nullptr; } + // We don't set the 3D texture state here or anything else, on some backends (?) + // a nextTexture_ of nullptr means keep the current texture. + return result; +} +TextureApplyResult TextureCacheCommon::ApplyTextureFinish(TexCacheEntry *entry, bool doBind) { + _dbg_assert_(entry); + TextureApplyResult result; nextTexture_ = nullptr; UpdateMaxSeenV(entry, gstate.isModeThrough()); diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index 6b0120d93b..90e221195e 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -406,7 +406,8 @@ protected: // This updates nextTexture_ / nextFramebufferTexture_, which is then used by ApplyTexture. // TODO: Return stuff directly instead of keeping state. - TextureApplyResult ApplyTextureFinish(bool doBind); + TextureApplyResult ApplyTextureFinish(TexCacheEntry *entry, bool doBind); + TextureApplyResult ApplyTextureFinishFramebuffer(VirtualFramebuffer *framebuffer, bool doBind); virtual void BindTexture(TexCacheEntry *entry) = 0; virtual void Unbind() = 0; virtual void ReleaseTexture(TexCacheEntry *entry, bool delete_them) = 0; From 40bce17f534d21c581b45fdd515d38ae11d88ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 16 Jul 2026 20:27:44 +0200 Subject: [PATCH 4/7] Some cleanup --- GPU/Common/TextureCacheCommon.cpp | 37 +------------------------------ GPU/Common/TextureCacheCommon.h | 7 +++--- GPU/D3D11/TextureCacheD3D11.cpp | 4 +++- GPU/GLES/TextureCacheGLES.cpp | 3 ++- GPU/Vulkan/TextureCacheVulkan.cpp | 3 ++- 5 files changed, 11 insertions(+), 43 deletions(-) diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 56603fe938..6b7ade5f25 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -1425,42 +1425,7 @@ void TextureCacheCommon::SetTextureFramebuffer(const AttachCandidate &candidate) nextNeedsRebuild_ = false; } -// Only looks for framebuffers. -bool TextureCacheCommon::SetOffsetTexture(u32 yOffset) { - if (!framebufferManager_->UseBufferedRendering()) { - return false; - } - - u32 texaddr = gstate.getTextureAddress(0); - GETextureFormat fmt = gstate.getTextureFormat(); - const u32 bpp = fmt == GE_TFMT_8888 ? 4 : 2; - const u32 texaddrOffset = yOffset * gstate.getTextureWidth(0) * bpp; - - if (!Memory::IsValidAddress(texaddr) || !Memory::IsValidAddress(texaddr + texaddrOffset)) { - return false; - } - - TextureDefinition def; - def.addr = texaddr; - def.format = fmt; - def.bufw = GetTextureBufw(0, texaddr, fmt); - def.dim = gstate.getTextureDimension(0); - - AttachCandidate bestCandidate; - if (GetBestFramebufferCandidate(framebufferManager_, def, texaddrOffset, &bestCandidate, "offsetTexture")) { - SetTextureFramebuffer(bestCandidate); - return true; - } else { - return false; - } -} - -bool TextureCacheCommon::GetCurrentFramebufferTextureDebug(GPUDebugBuffer &buffer, bool *isFramebuffer) { - if (!nextFramebufferTexture_) - return false; - *isFramebuffer = true; - - VirtualFramebuffer *vfb = nextFramebufferTexture_; +bool TextureCacheCommon::GetFramebufferTextureDebug(const VirtualFramebuffer *vfb, GPUDebugBuffer &buffer) { u8 sf = vfb->renderScaleFactor; int x = gstate_c.curTextureXOffset * sf; int y = gstate_c.curTextureYOffset * sf; diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index 90e221195e..b42b7161c0 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -347,7 +347,6 @@ public: TextureApplyResult ApplyTexture(bool doBind); void ApplySampler(const TextureApplyResult &result, bool flatZ, bool pixelMapped); // Should follow ApplyTexture - bool SetOffsetTexture(u32 yOffset); void Invalidate(u32 addr, int size, GPUInvalidationType type); void InvalidateAll(GPUInvalidationType type); void ClearNextFrame(); @@ -410,7 +409,9 @@ protected: TextureApplyResult ApplyTextureFinishFramebuffer(VirtualFramebuffer *framebuffer, bool doBind); virtual void BindTexture(TexCacheEntry *entry) = 0; virtual void Unbind() = 0; + virtual void BuildTexture(TexCacheEntry *const entry) = 0; virtual void ReleaseTexture(TexCacheEntry *entry, bool delete_them) = 0; + void SetTextureFramebuffer(const AttachCandidate &candidate); void DeleteTexture(TexCache::iterator it); void Decimate(const TexCacheEntry *const exceptThisOne, bool forcePressure); // forcePressure defaults to false. @@ -419,7 +420,6 @@ protected: virtual void ApplySamplerByKey(const SamplerCacheKey &key) = 0; void HandleTextureChange(TexCacheEntry *const entry, const char *reason, bool initialMatch, bool doDelete); - virtual void BuildTexture(TexCacheEntry *const entry) = 0; virtual void UpdateCurrentClut(GEPaletteFormat clutFormat, u32 clutBase, bool clutIndexIsSimple); // only overridden in GLES bool CheckFullHash(TexCacheEntry *entry, bool &doDelete); @@ -439,8 +439,7 @@ protected: void UpdateMaxSeenV(TexCacheEntry *entry, bool throughMode); - void SetTextureFramebuffer(const AttachCandidate &candidate); - bool GetCurrentFramebufferTextureDebug(GPUDebugBuffer &buffer, bool *isFramebuffer); + bool GetFramebufferTextureDebug(const VirtualFramebuffer *vfb, GPUDebugBuffer &buffer); virtual void BoundFramebufferTexture() {} diff --git a/GPU/D3D11/TextureCacheD3D11.cpp b/GPU/D3D11/TextureCacheD3D11.cpp index abc32eddf2..f35e6a5c9b 100644 --- a/GPU/D3D11/TextureCacheD3D11.cpp +++ b/GPU/D3D11/TextureCacheD3D11.cpp @@ -447,8 +447,10 @@ bool TextureCacheD3D11::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level // Apply texture may need to rebuild the texture if we're about to render, or bind a framebuffer. TextureApplyResult textureResult = ApplyTexture(true); if (textureResult.framebuffer) { - return GetCurrentFramebufferTextureDebug(buffer, isFramebuffer); + *isFramebuffer = true; + return GetFramebufferTextureDebug(textureResult.framebuffer, buffer); } + const TexCacheEntry *entry = textureResult.texCacheEntry; if (!entry) { return false; diff --git a/GPU/GLES/TextureCacheGLES.cpp b/GPU/GLES/TextureCacheGLES.cpp index a6a16dfa94..8494339cb1 100644 --- a/GPU/GLES/TextureCacheGLES.cpp +++ b/GPU/GLES/TextureCacheGLES.cpp @@ -367,7 +367,8 @@ bool TextureCacheGLES::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level, framebufferManagerGL_->RebindFramebuffer("RebindFramebuffer - GetCurrentTextureDebug"); TextureApplyResult textureResult = ApplyTexture(false); if (textureResult.framebuffer) { - return GetCurrentFramebufferTextureDebug(buffer, isFramebuffer); + *isFramebuffer = true; + return GetFramebufferTextureDebug(textureResult.framebuffer, buffer); } TexCacheEntry *entry = textureResult.texCacheEntry; diff --git a/GPU/Vulkan/TextureCacheVulkan.cpp b/GPU/Vulkan/TextureCacheVulkan.cpp index 6998358f01..eb8220f536 100644 --- a/GPU/Vulkan/TextureCacheVulkan.cpp +++ b/GPU/Vulkan/TextureCacheVulkan.cpp @@ -1005,7 +1005,8 @@ bool TextureCacheVulkan::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int leve // Apply texture may need to rebuild the texture if we're about to render, or bind a framebuffer. TextureApplyResult textureResult = ApplyTexture(true); if (textureResult.framebuffer) { - return GetCurrentFramebufferTextureDebug(buffer, isFramebuffer); + *isFramebuffer = true; + return GetFramebufferTextureDebug(textureResult.framebuffer, buffer); } TexCacheEntry *entry = textureResult.texCacheEntry; From 31c32c0ee087ad94280a7e6da5579c18bba41ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 16 Jul 2026 20:30:01 +0200 Subject: [PATCH 5/7] Remove the "nextFramebufferTexture_" variable --- GPU/Common/TextureCacheCommon.cpp | 13 ++++++------- GPU/Common/TextureCacheCommon.h | 3 +-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 6b7ade5f25..024389913f 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -717,8 +717,8 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { nextTexture_ = nullptr; nextNeedsRebuild_ = false; - SetTextureFramebuffer(bestCandidate); // sets curTexture3D - return ApplyTextureFinishFramebuffer(nextFramebufferTexture_, doBind); + VirtualFramebuffer *framebuffer = SetTextureFramebuffer(bestCandidate); // sets curTexture3D + return ApplyTextureFinishFramebuffer(framebuffer, doBind); } // Didn't match a framebuffer, keep going. @@ -789,7 +789,6 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { failedTexture_ = false; nextTexture_ = entry; - nextFramebufferTexture_ = nullptr; nextNeedsRehash_ = true; nextNeedsRebuild_ = true; return ApplyTextureFinish(entry, doBind); @@ -1339,7 +1338,7 @@ static bool MatchFramebuffer(const TextureDefinition &entry, } } -void TextureCacheCommon::SetTextureFramebuffer(const AttachCandidate &candidate) { +VirtualFramebuffer *TextureCacheCommon::SetTextureFramebuffer(const AttachCandidate &candidate) { VirtualFramebuffer *framebuffer = candidate.fb; RasterChannel channel = candidate.channel; @@ -1397,10 +1396,9 @@ void TextureCacheCommon::SetTextureFramebuffer(const AttachCandidate &candidate) WARN_LOG_ONCE(ndepthtex, Log::G3D, "Depth textures not supported, not binding"); // Flag to bind a null texture if we can't support depth textures. // Should only happen on old OpenGL. - nextFramebufferTexture_ = nullptr; + framebuffer = nullptr; failedTexture_ = true; } else { - nextFramebufferTexture_ = framebuffer; nextFramebufferTextureChannel_ = channel; } nextTexture_ = nullptr; @@ -1411,7 +1409,7 @@ void TextureCacheCommon::SetTextureFramebuffer(const AttachCandidate &candidate) } Unbind(); gstate_c.SetNeedShaderTexclamp(false); - nextFramebufferTexture_ = nullptr; + framebuffer = nullptr; nextTexture_ = nullptr; } @@ -1423,6 +1421,7 @@ void TextureCacheCommon::SetTextureFramebuffer(const AttachCandidate &candidate) nextNeedsRehash_ = false; nextNeedsChange_ = false; nextNeedsRebuild_ = false; + return framebuffer; } bool TextureCacheCommon::GetFramebufferTextureDebug(const VirtualFramebuffer *vfb, GPUDebugBuffer &buffer) { diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index b42b7161c0..ac3e37ffdb 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -411,7 +411,7 @@ protected: virtual void Unbind() = 0; virtual void BuildTexture(TexCacheEntry *const entry) = 0; virtual void ReleaseTexture(TexCacheEntry *entry, bool delete_them) = 0; - void SetTextureFramebuffer(const AttachCandidate &candidate); + VirtualFramebuffer *SetTextureFramebuffer(const AttachCandidate &candidate); void DeleteTexture(TexCache::iterator it); void Decimate(const TexCacheEntry *const exceptThisOne, bool forcePressure); // forcePressure defaults to false. @@ -477,7 +477,6 @@ protected: TexCacheEntry *nextTexture_ = nullptr; bool failedTexture_ = false; - VirtualFramebuffer *nextFramebufferTexture_ = nullptr; RasterChannel nextFramebufferTextureChannel_ = RASTER_COLOR; u32 clutHash_ = 0; From ff89e4da4d5fd90855eda388edc663cf3facdb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 16 Jul 2026 20:38:27 +0200 Subject: [PATCH 6/7] Simplify --- GPU/Common/TextureCacheCommon.cpp | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 024389913f..5dcda8829b 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -541,6 +541,7 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { bool clutInShader = false; u32 cluthash; if (hasClut) { + // Check if we should use dynamic CLUT in shader or some other tricks. if (PSP_CoreParameter().compat.flags().TextureCLUTInShader && !replacer_.Enabled() && (texFormat == GE_TFMT_CLUT8 || texFormat == GE_TFMT_CLUT4)) { if (clutLastFormat_ != gstate.clutformat) { // We update here because the clut format can be specified after the load. @@ -666,16 +667,6 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { // got one! gstate_c.curTextureWidth = w; gstate_c.curTextureHeight = h; - gstate_c.SetTextureIsVideo(false); - gstate_c.SetTextureIs3D(entry->status & TexStatus::IS_3D); - gstate_c.SetTextureIsArray(false); - gstate_c.SetTextureIsFramebuffer(false); - if (entry->status & TexStatus::CLUT8_INDEXED) { - gstate_c.SetShaderDepal(ShaderDepalMode::NORMAL, GE_FORMAT_CLUT8); - } else { - gstate_c.SetShaderDepal(ShaderDepalMode::OFF, GE_FORMAT_INVALID); - } - if (rehash) { // Update in case any of these changed. entry->bufw = bufw; @@ -777,15 +768,6 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { gstate_c.curTextureWidth = w; gstate_c.curTextureHeight = h; - gstate_c.SetTextureIsVideo((entry->status & TexStatus::VIDEO) != 0); - gstate_c.SetTextureIs3D((entry->status & TexStatus::IS_3D) != 0); - gstate_c.SetTextureIsArray(false); // Ordinary 2D textures still aren't used by array view in VK. We probably might as well, though, at this point.. - gstate_c.SetTextureIsFramebuffer(false); - if (entry->status & TexStatus::CLUT8_INDEXED) { - gstate_c.SetShaderDepal(ShaderDepalMode::NORMAL, GE_FORMAT_CLUT8); - } else { - gstate_c.SetShaderDepal(ShaderDepalMode::OFF, GE_FORMAT_INVALID); - } failedTexture_ = false; nextTexture_ = entry; @@ -814,6 +796,7 @@ TextureApplyResult TextureCacheCommon::ApplyTextureFinishFramebuffer(VirtualFram TextureApplyResult TextureCacheCommon::ApplyTextureFinish(TexCacheEntry *entry, bool doBind) { _dbg_assert_(entry); + TextureApplyResult result; nextTexture_ = nullptr; @@ -867,6 +850,8 @@ TextureApplyResult TextureCacheCommon::ApplyTextureFinish(TexCacheEntry *entry, } gstate_c.SetTextureIsVideo((entry->status & TexStatus::VIDEO) != 0); + gstate_c.SetTextureIsArray(false); // Ordinary 2D textures still aren't used by array view in VK. We probably might as well, though, at this point.. + gstate_c.SetTextureIsFramebuffer(false); entry->lastFrame = gpuStats.totals.numFlips; if (entry->status & TexStatus::CLUT_GPU) { _dbg_assert_(entry->status & TexStatus::CLUT8_INDEXED); @@ -876,7 +861,6 @@ TextureApplyResult TextureCacheCommon::ApplyTextureFinish(TexCacheEntry *entry, } gstate_c.SetTextureSolidAlpha(false); gstate_c.SetTextureIs3D(false); - gstate_c.SetTextureIsArray(false); return TextureApplyResult{}; } else { if (doBind) { @@ -884,7 +868,6 @@ TextureApplyResult TextureCacheCommon::ApplyTextureFinish(TexCacheEntry *entry, } gstate_c.SetTextureSolidAlpha((entry->status & TexStatus::ALPHA_SOLID) != 0); gstate_c.SetTextureIs3D((entry->status & TexStatus::IS_3D) != 0); - gstate_c.SetTextureIsArray(false); if (entry->status & TexStatus::CLUT8_INDEXED) { bool smoothedDepal = false; u32 depthUpperBits = 0; @@ -892,7 +875,7 @@ TextureApplyResult TextureCacheCommon::ApplyTextureFinish(TexCacheEntry *entry, BindAsClutTexture(clutTexture.texture, false); gstate_c.SetShaderDepal(ShaderDepalMode::NORMAL, GE_FORMAT_CLUT8); } else { - gstate_c.SetShaderDepal(ShaderDepalMode::OFF); + gstate_c.SetShaderDepal(ShaderDepalMode::OFF, GE_FORMAT_INVALID); } } return TextureApplyResult{entry, nullptr}; @@ -2526,7 +2509,8 @@ void TextureCacheCommon::ApplyTextureFramebuffer(VirtualFramebuffer *framebuffer } // Applies depal to a normal (non-framebuffer) texture, pre-decoded to CLUT8 format. -// TODO: Merge this function with the above. +// TODO: Merge this function with the above. Or rather, we should try to eliminate this in favor +// of in-shader depal. void TextureCacheCommon::ApplyTextureDepalFramebufferCLUT(const TexCacheEntry *const entry) { uint32_t clutMode = gstate.clutformat & 0xFFFFFF; From 3b58242063822476e989ffbadc5b9b0a9b293659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 16 Jul 2026 20:54:58 +0200 Subject: [PATCH 7/7] More cleanup --- GPU/Common/TextureCacheCommon.cpp | 17 ++++------------- GPU/Common/TextureCacheCommon.h | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 5dcda8829b..ca174129f1 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -536,11 +536,10 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { texFormat = GE_TFMT_5650; } - bool hasClut = gstate.isTextureFormatIndexed(); bool hasClutGPU = false; bool clutInShader = false; u32 cluthash; - if (hasClut) { + if (gstate.isTextureFormatIndexed()) { // Check if we should use dynamic CLUT in shader or some other tricks. if (PSP_CoreParameter().compat.flags().TextureCLUTInShader && !replacer_.Enabled() && (texFormat == GE_TFMT_CLUT8 || texFormat == GE_TFMT_CLUT4)) { if (clutLastFormat_ != gstate.clutformat) { @@ -567,7 +566,7 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { } else { cluthash = 0; } - u64 cachekey = TexCacheEntry::CacheKey(texaddr, texFormat, dim, cluthash); + const u64 cachekey = TexCacheEntry::CacheKey(texaddr, texFormat, dim, cluthash); int bufw = GetTextureBufw(0, texaddr, texFormat); u8 maxLevel = gstate.getTextureMaxLevel(); @@ -619,12 +618,6 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { entry->status &= ~(TexStatus::HASH_RECHECK | TexStatus::CLUT_RECHECK); } - // Do we need to recreate? - if (entry->status & TexStatus::FORCE_REBUILD) { - match = false; - entry->status &= ~TexStatus::FORCE_REBUILD; - } - if (match && (entry->status & TexStatus::TO_SCALE) && (standardScaleFactor_ > 1 || shaderScaleFactor_ > 1) && texelsScaledThisFrame_ < TEXCACHE_MAX_TEXELS_SCALED) { // INFO_LOG(Log::G3D, "Reloading texture to do the scaling we skipped.."); match = false; @@ -729,7 +722,7 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { entry->status |= TexStatus::CLUT_GPU | TexStatus::CLUT8_INDEXED; } - if (hasClut && clutRenderAddress_ == 0xFFFFFFFF) { + if (gstate.isTextureFormatIndexed() && clutRenderAddress_ == 0xFFFFFFFF) { const u64 cachekeyMin = (u64)(texaddr & 0x3FFFFFFF) << 32; const u64 cachekeyMax = cachekeyMin + (1ULL << 32); @@ -2868,6 +2861,7 @@ bool TextureCacheCommon::PrepareBuildTexture(BuildTexturePlan &plan, TexCacheEnt plan.w = gstate.getTextureWidth(0); plan.h = gstate.getTextureHeight(0); + // TODO: We should move the PPGE texture entirely out from the PSP's memory space, so we don't save it in every save state. const bool isPPGETexture = entry->addr >= PSP_GetKernelMemoryBase() && entry->addr < PSP_GetKernelMemoryEnd(); // Don't scale the PPGe texture. @@ -3118,9 +3112,6 @@ std::string TexStatusToString(TexStatus status) { if (status & TexStatus::CLUT8_INDEXED) { result += "CLUT8_INDEXED "; } - if (status & TexStatus::FORCE_REBUILD) { - result += "FORCE_REBUILD "; - } if (status & TexStatus::IS_3D) { result += "3D "; } diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index ac3e37ffdb..9af052586b 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -150,7 +150,7 @@ enum class TexStatus : u16 { IS_3D = (1 << 10), NO_MIPS = (1 << 11), // Has bad or unusable mipmap levels. FRAMEBUFFER_OVERLAP = (1 << 12), - FORCE_REBUILD = (1 << 13), + // Free bit 13 CLUT8_INDEXED = (1 << 14), // Decoded as plain CLUT8 indices instead of all the way to colors. CLUT_GPU = (1 << 15), };