mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-07 13:13:24 +02:00
TextureCacheGLES passed a hardcoded 0.0f instead of key.aniso, so the
Anisotropic Filtering setting did nothing at all on the OpenGL backend, even
though GPU_USE_ANISOTROPY was advertised and D3D11/Vulkan both honor it. Looks
like it was left behind by the 2017 render manager refactor.
The queue runner now clamps to the device maximum it already queried into
maxAnisotropyLevel_ (until now unused), and only touches the parameter when the
extension is actually supported - the anisotropy branch there has been dead
since every caller passed 0.0f, so this is the first time it runs.
0.0f keeps its meaning of "don't care" for the CLUT/fragment-test/thin3d
callers; the texture cache now passes 1.0f when the setting is off, so turning
it off takes effect on already-uploaded textures instead of only new ones.
TexCache: Never use anisotropic filtering for CLUT8-indexed textures
What gets sampled for those is palette indices, depalettized by the shader
afterwards - averaging indices across an anisotropic footprint produces garbage
colors. Affects all backends, not just the GL one that just started honoring
key.aniso.
TexCache: Clear key.aniso wherever filtering is forced to nearest
It was only cleared in the two places inside the AUTO_MAX_QUALITY branch, so the
TEX_FILTER_AUTO path (pixel-mapped textures, the ugly color test heuristic), the
FORCE_NEAREST setting and the replacement-texture override could all end up
requesting nearest filtering with anisotropy still on.
Doing it in the switch that applies forceFiltering covers every path, so it
can't drift apart again.
GLES: Only record the applied anisotropy, and log skipped draws
The queue runner updated tex->anisotropy even when it skipped the call because
the value was 0.0f ("don't care") - harmless while nothing ever set anisotropy,
but now it would make the tracked state disagree with GL, so a later request for
the value it thinks is set would be wrongly skipped.
Also log when a draw is skipped for a missing vertex shader. The failure is
cached per shader ID, so without it geometry silently disappears for the rest of
the session after the one-shot OSD message.
421 lines
14 KiB
C++
421 lines
14 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 <cstring>
|
|
|
|
#include "ext/xxhash.h"
|
|
#include "Common/Common.h"
|
|
#include "Common/Data/Convert/ColorConv.h"
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/Profiler/Profiler.h"
|
|
#include "Common/System/OSD.h"
|
|
#include "Common/GPU/OpenGL/GLRenderManager.h"
|
|
#include "Common/TimeUtil.h"
|
|
|
|
#include "Core/Config.h"
|
|
|
|
#include "GPU/ge_constants.h"
|
|
#include "GPU/GPUState.h"
|
|
#include "GPU/GPUDefinitions.h"
|
|
#include "GPU/GLES/TextureCacheGLES.h"
|
|
#include "GPU/GLES/FramebufferManagerGLES.h"
|
|
#include "GPU/Common/TextureShaderCommon.h"
|
|
#include "GPU/Common/DrawEngineCommon.h"
|
|
|
|
TextureCacheGLES::TextureCacheGLES(Draw::DrawContext *draw, Draw2D *draw2D)
|
|
: TextureCacheCommon(draw, draw2D) {
|
|
render_ = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
|
|
}
|
|
|
|
TextureCacheGLES::~TextureCacheGLES() {
|
|
Clear(true);
|
|
}
|
|
|
|
void TextureCacheGLES::SetFramebufferManager(FramebufferManagerGLES *fbManager) {
|
|
framebufferManagerGL_ = fbManager;
|
|
framebufferManager_ = fbManager;
|
|
}
|
|
|
|
void TextureCacheGLES::ReleaseTexture(TexCacheEntry *entry, bool delete_them) {
|
|
if (delete_them) {
|
|
if (entry->textureName) {
|
|
render_->DeleteTexture(entry->textureName);
|
|
}
|
|
}
|
|
entry->textureName = nullptr;
|
|
}
|
|
|
|
void TextureCacheGLES::Clear(bool delete_them) {
|
|
TextureCacheCommon::Clear(delete_them);
|
|
}
|
|
|
|
static Draw::DataFormat getClutDestFormat(GEPaletteFormat format) {
|
|
switch (format) {
|
|
case GE_CMODE_16BIT_ABGR4444:
|
|
return Draw::DataFormat::R4G4B4A4_UNORM_PACK16;
|
|
case GE_CMODE_16BIT_ABGR5551:
|
|
return Draw::DataFormat::R5G5B5A1_UNORM_PACK16;
|
|
case GE_CMODE_16BIT_BGR5650:
|
|
return Draw::DataFormat::R5G6B5_UNORM_PACK16;
|
|
case GE_CMODE_32BIT_ABGR8888:
|
|
return Draw::DataFormat::R8G8B8A8_UNORM;
|
|
}
|
|
return Draw::DataFormat::UNDEFINED;
|
|
}
|
|
|
|
static constexpr GLuint MinFiltGL[8] = {
|
|
GL_NEAREST,
|
|
GL_LINEAR,
|
|
GL_NEAREST,
|
|
GL_LINEAR,
|
|
GL_NEAREST_MIPMAP_NEAREST,
|
|
GL_LINEAR_MIPMAP_NEAREST,
|
|
GL_NEAREST_MIPMAP_LINEAR,
|
|
GL_LINEAR_MIPMAP_LINEAR,
|
|
};
|
|
|
|
static constexpr GLuint MagFiltGL[2] = {
|
|
GL_NEAREST,
|
|
GL_LINEAR
|
|
};
|
|
|
|
void TextureCacheGLES::ApplySamplerByKey(const SamplerCacheKey &key) {
|
|
if (gstate_c.Use(GPU_USE_SAMPLER_LOD_CONTROL)) {
|
|
float minLod = (float)key.minLevel / 256.0f;
|
|
float maxLod = (float)key.maxLevel / 256.0f;
|
|
float lodBias = (float)key.lodBias / 256.0f;
|
|
render_->SetTextureLod(0, minLod, maxLod, lodBias);
|
|
}
|
|
|
|
// 1.0 means no anisotropic filtering. The queue runner clamps to the device maximum.
|
|
float aniso = key.aniso ? (float)(1 << g_Config.iAnisotropyLevel) : 1.0f;
|
|
int minKey = ((int)key.mipEnable << 2) | ((int)key.mipFilt << 1) | ((int)key.minFilt);
|
|
render_->SetTextureSampler(0,
|
|
key.sClamp ? GL_CLAMP_TO_EDGE : GL_REPEAT, key.tClamp ? GL_CLAMP_TO_EDGE : GL_REPEAT,
|
|
key.magFilt ? GL_LINEAR : GL_NEAREST, MinFiltGL[minKey], aniso);
|
|
}
|
|
|
|
static void ConvertColors(void *dstBuf, const void *srcBuf, Draw::DataFormat dstFmt, int numPixels) {
|
|
const u32 *src = (const u32 *)srcBuf;
|
|
u32 *dst = (u32 *)dstBuf;
|
|
switch (dstFmt) {
|
|
case Draw::DataFormat::R4G4B4A4_UNORM_PACK16:
|
|
ConvertRGBA4444ToABGR4444((u16 *)dst, (const u16 *)src, numPixels);
|
|
break;
|
|
// Final Fantasy 2 uses this heavily in animated textures.
|
|
case Draw::DataFormat::R5G5B5A1_UNORM_PACK16:
|
|
ConvertRGBA5551ToABGR1555((u16 *)dst, (const u16 *)src, numPixels);
|
|
break;
|
|
case Draw::DataFormat::R5G6B5_UNORM_PACK16:
|
|
ConvertRGB565ToBGR565((u16 *)dst, (const u16 *)src, numPixels);
|
|
break;
|
|
default:
|
|
// No need to convert RGBA8888, right order already
|
|
if (dst != src)
|
|
memcpy(dst, src, numPixels * sizeof(u32));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// TODO: This is almost the same as the Common one, just with some extra color conversion. Should merge.
|
|
void TextureCacheGLES::UpdateCurrentClut(GEPaletteFormat clutFormat, u32 clutBase, bool clutIndexIsSimple) {
|
|
const u32 clutBaseBytes = clutFormat == GE_CMODE_32BIT_ABGR8888 ? (clutBase * sizeof(u32)) : (clutBase * sizeof(u16));
|
|
// Technically, these extra bytes weren't loaded, but hopefully it was loaded earlier.
|
|
// If not, we're going to hash random data, which hopefully doesn't cause a performance issue.
|
|
//
|
|
// TODO: Actually, this seems like a hack. The game can upload part of a CLUT and reference other data.
|
|
// clutTotalBytes_ is the last amount uploaded. We should hash clutMaxBytes_, but this will often hash
|
|
// unrelated old entries for small palettes.
|
|
// Adding clutBaseBytes may just be mitigating this for some usage patterns.
|
|
const u32 clutExtendedBytes = std::min(clutTotalBytes_ + clutBaseBytes, clutMaxBytes_);
|
|
|
|
if (replacer_.Enabled())
|
|
clutHash_ = XXH32((const char *)clutBufRaw_, clutExtendedBytes, 0xC0108888);
|
|
else
|
|
clutHash_ = XXH3_64bits((const char *)clutBufRaw_, clutExtendedBytes) & 0xFFFFFFFF;
|
|
|
|
// Avoid a copy when we don't need to convert colors.
|
|
if (clutFormat != GE_CMODE_32BIT_ABGR8888) {
|
|
const int numColors = clutFormat == GE_CMODE_32BIT_ABGR8888 ? (clutMaxBytes_ / sizeof(u32)) : (clutMaxBytes_ / sizeof(u16));
|
|
ConvertColors(clutBufConverted_, clutBufRaw_, getClutDestFormat(clutFormat), numColors);
|
|
clutBuf_ = clutBufConverted_;
|
|
} else {
|
|
clutBuf_ = clutBufRaw_;
|
|
}
|
|
|
|
// Special optimization: fonts typically draw clut4 with just alpha values in a single color.
|
|
bool alphaLinear = false;
|
|
u16 alphaLinearColor = 0;
|
|
if (clutFormat == GE_CMODE_16BIT_ABGR4444 && clutIndexIsSimple) {
|
|
const u16 *clut = (const u16 *)(clutBuf_);
|
|
alphaLinear = true;
|
|
alphaLinearColor = clut[15] & 0xFFF0;
|
|
for (int i = 0; i < 16; ++i) {
|
|
u16 step = alphaLinearColor | i;
|
|
if (clut[i] != step) {
|
|
alphaLinear = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
clutProperties_.clutAlphaLinear = alphaLinear;
|
|
clutProperties_.clutAlphaLinearColor = alphaLinearColor;
|
|
|
|
clutLastFormat_ = gstate.clutformat;
|
|
}
|
|
|
|
void TextureCacheGLES::BindTexture(TexCacheEntry *entry) {
|
|
if (!entry) {
|
|
render_->BindTexture(0, nullptr);
|
|
lastBoundTexture_ = nullptr;
|
|
return;
|
|
}
|
|
if (entry->textureName != lastBoundTexture_) {
|
|
render_->BindTexture(0, entry->textureName);
|
|
lastBoundTexture_ = entry->textureName;
|
|
}
|
|
}
|
|
|
|
void TextureCacheGLES::Unbind() {
|
|
render_->BindTexture(TEX_SLOT_PSP_TEXTURE, nullptr);
|
|
ForgetLastTexture();
|
|
}
|
|
|
|
void TextureCacheGLES::BindAsClutTexture(Draw::Texture *tex, bool smooth) {
|
|
GLRTexture *glrTex = (GLRTexture *)draw_->GetNativeObject(Draw::NativeObject::TEXTURE_VIEW, tex);
|
|
render_->BindTexture(TEX_SLOT_CLUT, glrTex);
|
|
render_->SetTextureSampler(TEX_SLOT_CLUT, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, smooth ? GL_LINEAR : GL_NEAREST, smooth ? GL_LINEAR : GL_NEAREST, 0.0f);
|
|
}
|
|
|
|
void TextureCacheGLES::BuildTexture(TexCacheEntry *const entry) {
|
|
BuildTexturePlan plan;
|
|
if (!PrepareBuildTexture(plan, entry)) {
|
|
// We're screwed?
|
|
return;
|
|
}
|
|
|
|
_assert_(!entry->textureName);
|
|
|
|
// GLES2 doesn't have support for a "Max lod" which is critical as PSP games often
|
|
// don't specify mips all the way down. As a result, we either need to manually generate
|
|
// the bottom few levels or rely on OpenGL's autogen mipmaps instead, which might not
|
|
// be as good quality as the game's own (might even be better in some cases though).
|
|
|
|
int tw = plan.createW;
|
|
int th = plan.createH;
|
|
|
|
Draw::DataFormat dstFmt = GetDestFormat(GETextureFormat(entry->format), gstate.getClutPaletteFormat());
|
|
if (plan.doReplace) {
|
|
plan.replaced->GetSize(plan.baseLevelSrc, &tw, &th);
|
|
dstFmt = plan.replaced->Format();
|
|
} else if (plan.scaleFactor > 1 || plan.saveTexture) {
|
|
dstFmt = Draw::DataFormat::R8G8B8A8_UNORM;
|
|
} else if (plan.decodeToClut8) {
|
|
dstFmt = Draw::DataFormat::R8_UNORM;
|
|
}
|
|
|
|
if (plan.depth == 1) {
|
|
entry->textureName = render_->CreateTexture(GL_TEXTURE_2D, tw, th, 1, plan.levelsToCreate);
|
|
} else {
|
|
_dbg_assert_(draw_->GetDeviceCaps().texture3DSupported);
|
|
entry->textureName = render_->CreateTexture(GL_TEXTURE_3D, tw, th, plan.depth, 1);
|
|
}
|
|
|
|
// Apply some additional compatibility checks.
|
|
if (plan.levelsToLoad > 1) {
|
|
// Avoid PowerVR driver bug
|
|
if (plan.w > 1 && plan.h > 1 && !(plan.h > plan.w && draw_->GetBugs().Has(Draw::Bugs::PVR_GENMIPMAP_HEIGHT_GREATER))) { // Really! only seems to fail if height > width
|
|
// It's ok to generate mipmaps beyond the loaded levels.
|
|
} else {
|
|
plan.levelsToCreate = plan.levelsToLoad;
|
|
}
|
|
}
|
|
|
|
if (!gstate_c.Use(GPU_USE_SAMPLER_LOD_CONTROL)) {
|
|
// If the mip chain is not full..
|
|
if (plan.levelsToCreate != plan.maxPossibleLevels) {
|
|
// We need to avoid creating mips at all, or generate them all - can't be incomplete
|
|
// on this hardware (strict OpenGL rules).
|
|
plan.levelsToCreate = 1;
|
|
plan.levelsToLoad = 1;
|
|
entry->status |= TexStatus::NO_MIPS;
|
|
}
|
|
}
|
|
|
|
if (plan.depth == 1) {
|
|
for (int i = 0; i < plan.levelsToLoad; i++) {
|
|
int srcLevel = i == 0 ? plan.baseLevelSrc : i;
|
|
|
|
int mipWidth;
|
|
int mipHeight;
|
|
plan.GetMipSize(i, &mipWidth, &mipHeight);
|
|
|
|
u8 *data = nullptr;
|
|
int stride = 0;
|
|
int dataSize;
|
|
|
|
bool bc = false;
|
|
|
|
if (plan.doReplace) {
|
|
int blockSize = 0;
|
|
if (Draw::DataFormatIsBlockCompressed(plan.replaced->Format(), &blockSize)) {
|
|
stride = mipWidth * 4;
|
|
dataSize = plan.replaced->GetLevelDataSizeAfterCopy(i);
|
|
bc = true;
|
|
} else {
|
|
int bpp = (int)Draw::DataFormatSizeInBytes(plan.replaced->Format());
|
|
stride = mipWidth * bpp;
|
|
dataSize = stride * mipHeight;
|
|
}
|
|
} else {
|
|
int bpp = 0;
|
|
if (plan.scaleFactor > 1) {
|
|
bpp = 4;
|
|
} else {
|
|
bpp = (int)Draw::DataFormatSizeInBytes(dstFmt);
|
|
}
|
|
stride = mipWidth * bpp;
|
|
dataSize = stride * mipHeight;
|
|
}
|
|
|
|
data = (u8 *)AllocateAlignedMemory(dataSize, 16);
|
|
_assert_msg_(data != nullptr, "Failed to allocate aligned memory for texture level %d: %d bytes (%dx%d)", i, (int)dataSize, mipWidth, mipHeight);
|
|
|
|
if (!data) {
|
|
ERROR_LOG(Log::G3D, "Ran out of RAM trying to allocate a temporary texture upload buffer (%dx%d)", mipWidth, mipHeight);
|
|
return;
|
|
}
|
|
|
|
LoadTextureLevel(*entry, data, dataSize, stride, plan, srcLevel, dstFmt, TexDecodeFlags::REVERSE_COLORS);
|
|
|
|
// NOTE: TextureImage takes ownership of data, so we don't free it afterwards.
|
|
render_->TextureImage(entry->textureName, i, mipWidth, mipHeight, 1, dstFmt, data, GLRAllocType::ALIGNED);
|
|
}
|
|
|
|
bool genMips = plan.levelsToCreate > plan.levelsToLoad;
|
|
|
|
render_->FinalizeTexture(entry->textureName, plan.levelsToLoad, genMips);
|
|
} else {
|
|
int bpp = (int)Draw::DataFormatSizeInBytes(dstFmt);
|
|
int stride = bpp * (plan.w * plan.scaleFactor);
|
|
int levelStride = stride * (plan.h * plan.scaleFactor);
|
|
|
|
size_t dataSize = levelStride * plan.depth;
|
|
u8 *data = (u8 *)AllocateAlignedMemory(dataSize, 16);
|
|
_assert_msg_(data != nullptr, "Failed to allocate aligned memory for 3d texture: %d bytes", (int)dataSize);
|
|
if (!data) {
|
|
ERROR_LOG(Log::G3D, "Ran out of RAM trying to allocate a temporary 3D texture upload buffer (%dx%dx%d)", plan.w, plan.h, plan.depth);
|
|
return;
|
|
}
|
|
|
|
memset(data, 0, levelStride * plan.depth);
|
|
u8 *p = data;
|
|
|
|
for (int i = 0; i < plan.depth; i++) {
|
|
LoadTextureLevel(*entry, p, dataSize, stride, plan, i, dstFmt, TexDecodeFlags::REVERSE_COLORS);
|
|
p += levelStride;
|
|
}
|
|
|
|
render_->TextureImage(entry->textureName, 0, plan.w * plan.scaleFactor, plan.h * plan.scaleFactor, plan.depth, dstFmt, data, GLRAllocType::ALIGNED);
|
|
|
|
// Signal that we support depth textures so use it as one.
|
|
entry->status |= TexStatus::IS_3D;
|
|
|
|
render_->FinalizeTexture(entry->textureName, 1, false);
|
|
}
|
|
|
|
if (plan.doReplace) {
|
|
entry->SetAlphaStatus(plan.replaced->AlphaStatus());
|
|
}
|
|
}
|
|
|
|
Draw::DataFormat TextureCacheGLES::GetDestFormat(GETextureFormat format, GEPaletteFormat clutFormat) {
|
|
switch (format) {
|
|
case GE_TFMT_CLUT4:
|
|
case GE_TFMT_CLUT8:
|
|
case GE_TFMT_CLUT16:
|
|
case GE_TFMT_CLUT32:
|
|
return getClutDestFormat(clutFormat);
|
|
case GE_TFMT_4444:
|
|
return Draw::DataFormat::R4G4B4A4_UNORM_PACK16;
|
|
case GE_TFMT_5551:
|
|
return Draw::DataFormat::R5G5B5A1_UNORM_PACK16;
|
|
case GE_TFMT_5650:
|
|
return Draw::DataFormat::R5G6B5_UNORM_PACK16;
|
|
case GE_TFMT_8888:
|
|
case GE_TFMT_DXT1:
|
|
case GE_TFMT_DXT3:
|
|
case GE_TFMT_DXT5:
|
|
default:
|
|
return Draw::DataFormat::R8G8B8A8_UNORM;
|
|
}
|
|
}
|
|
|
|
bool TextureCacheGLES::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level, bool *isFramebuffer) {
|
|
ForgetLastTexture();
|
|
|
|
// 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) {
|
|
*isFramebuffer = true;
|
|
return GetFramebufferTextureDebug(textureResult.framebuffer, textureResult.framebufferTextureChannel, buffer);
|
|
}
|
|
|
|
TexCacheEntry *entry = textureResult.texCacheEntry;
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
|
|
GLRenderManager *renderManager = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
|
|
|
|
// Not a framebuffer, so let's assume these are right.
|
|
// TODO: But they may definitely not be, if the texture was scaled.
|
|
int w = gstate.getTextureWidth(level);
|
|
int h = gstate.getTextureHeight(level);
|
|
|
|
bool result = entry->textureName != nullptr;
|
|
if (result) {
|
|
buffer.Allocate(w, h, GE_FORMAT_8888, false);
|
|
renderManager->CopyImageToMemorySync(entry->textureName, level, 0, 0, w, h, Draw::DataFormat::R8G8B8A8_UNORM, (uint8_t *)buffer.GetData(), w, "GetCurrentTextureDebug");
|
|
} else {
|
|
ERROR_LOG(Log::G3D, "Failed to get debug texture: texture is null");
|
|
}
|
|
gstate_c.Dirty(DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS);
|
|
framebufferManager_->RebindFramebuffer("RebindFramebuffer - GetCurrentTextureDebug");
|
|
|
|
*isFramebuffer = false;
|
|
return result;
|
|
}
|
|
|
|
void TextureCacheGLES::DeviceLost() {
|
|
TextureCacheCommon::DeviceLost();
|
|
draw_ = nullptr;
|
|
render_ = nullptr;
|
|
}
|
|
|
|
void TextureCacheGLES::DeviceRestore(Draw::DrawContext *draw) {
|
|
TextureCacheCommon::DeviceRestore(draw);
|
|
render_ = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
|
|
}
|
|
|
|
void *TextureCacheGLES::GetNativeTextureView(const TexCacheEntry *entry, bool flat) const {
|
|
GLRTexture *tex = entry->textureName;
|
|
return (void *)tex;
|
|
}
|