Files
ppsspp/GPU/GLES/DrawEngineGLES.cpp
T
Henrik Rydgård 89ebb8acc6 GLES: Actually apply anisotropic filtering
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.
2026-09-04 10:41:15 -06:00

460 lines
18 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 "Common/LogReporting.h"
#include "Common/GPU/OpenGL/GLDebugLog.h"
#include "Common/Profiler/Profiler.h"
#include "GPU/GPUState.h"
#include "GPU/ge_constants.h"
#include "GPU/Common/SplineCommon.h"
#include "GPU/Common/VertexDecoderCommon.h"
#include "GPU/Common/SoftwareTransformCommon.h"
#include "GPU/GLES/DrawEngineGLES.h"
#include "GPU/GLES/ShaderManagerGLES.h"
#include "GPU/GLES/GPU_GLES.h"
#include "GPU/GLES/FramebufferManagerGLES.h"
static const GLuint glprim[8] = {
// Points, which are expanded to triangles.
GL_TRIANGLES,
// Lines and line strips, which are also expanded to triangles.
GL_TRIANGLES,
GL_TRIANGLES,
GL_TRIANGLES,
GL_TRIANGLE_STRIP,
GL_TRIANGLE_FAN,
// Rectangles, which are expanded to triangles.
GL_TRIANGLES,
};
enum {
TRANSFORMED_VERTEX_BUFFER_SIZE = VERTEX_BUFFER_MAX * sizeof(TransformedVertex)
};
DrawEngineGLES::DrawEngineGLES(Draw::DrawContext *draw) : inputLayoutMap_(16), draw_(draw) {
render_ = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
decOptions_.expand8BitNormalsToFloat = false;
InitDeviceObjects();
}
DrawEngineGLES::~DrawEngineGLES() {
DestroyDeviceObjects();
}
void DrawEngineGLES::DeviceLost() {
DestroyDeviceObjects();
draw_ = nullptr;
render_ = nullptr;
}
void DrawEngineGLES::DeviceRestore(Draw::DrawContext *draw) {
draw_ = draw;
render_ = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
InitDeviceObjects();
}
void DrawEngineGLES::InitDeviceObjects() {
_assert_msg_(render_ != nullptr, "Render manager must be set");
for (int i = 0; i < GLRenderManager::MAX_INFLIGHT_FRAMES; i++) {
frameData_[i].pushVertex = render_->CreatePushBuffer(i, GL_ARRAY_BUFFER, 2 * 1024 * 1024, 256, "game_vertex");
frameData_[i].pushIndex = render_->CreatePushBuffer(i, GL_ELEMENT_ARRAY_BUFFER, 256 * 1024, 64, "game_index");
}
int stride = sizeof(TransformedVertex);
std::vector<GLRInputLayout::Entry> entries;
entries.push_back({ ATTR_POSITION, 4, GL_FLOAT, GL_FALSE, offsetof(TransformedVertex, x) });
entries.push_back({ ATTR_TEXCOORD, 3, GL_FLOAT, GL_FALSE, offsetof(TransformedVertex, u) });
entries.push_back({ ATTR_COLOR0, 4, GL_UNSIGNED_BYTE, GL_TRUE, offsetof(TransformedVertex, color0_32) });
entries.push_back({ ATTR_COLOR1, 3, GL_UNSIGNED_BYTE, GL_TRUE, offsetof(TransformedVertex, color1_32) });
entries.push_back({ ATTR_NORMAL, 1, GL_FLOAT, GL_FALSE, offsetof(TransformedVertex, fog) });
softwareInputLayout_ = render_->CreateInputLayout(entries, stride);
draw_->SetInvalidationCallback(std::bind(&DrawEngineGLES::Invalidate, this, std::placeholders::_1));
}
void DrawEngineGLES::DestroyDeviceObjects() {
if (!draw_) {
return;
}
draw_->SetInvalidationCallback(InvalidationCallback());
// Beware: this could be called twice in a row, sometimes.
for (int i = 0; i < GLRenderManager::MAX_INFLIGHT_FRAMES; i++) {
if (!frameData_[i].pushVertex && !frameData_[i].pushIndex)
continue;
if (frameData_[i].pushVertex)
render_->DeletePushBuffer(frameData_[i].pushVertex);
if (frameData_[i].pushIndex)
render_->DeletePushBuffer(frameData_[i].pushIndex);
frameData_[i].pushVertex = nullptr;
frameData_[i].pushIndex = nullptr;
}
if (softwareInputLayout_)
render_->DeleteInputLayout(softwareInputLayout_);
softwareInputLayout_ = nullptr;
ClearInputLayoutMap();
}
void DrawEngineGLES::ClearInputLayoutMap() {
inputLayoutMap_.Iterate([&](const uint32_t &key, GLRInputLayout *il) {
render_->DeleteInputLayout(il);
});
inputLayoutMap_.Clear();
}
void DrawEngineGLES::BeginFrame() {
DrawEngineCommon::BeginFrame();
FrameData &frameData = frameData_[render_->GetCurFrame()];
frameData.pushIndex->Begin();
frameData.pushVertex->Begin();
lastRenderStepId_ = -1;
}
void DrawEngineGLES::EndFrame() {
FrameData &frameData = frameData_[render_->GetCurFrame()];
frameData.pushIndex->End();
frameData.pushVertex->End();
}
struct GlTypeInfo {
u16 type;
u8 count;
u8 normalized;
};
static const GlTypeInfo GLComp[] = {
{0}, // DEC_NONE,
{GL_FLOAT, 1, GL_FALSE}, // DEC_FLOAT_1,
{GL_FLOAT, 2, GL_FALSE}, // DEC_FLOAT_2,
{GL_FLOAT, 3, GL_FALSE}, // DEC_FLOAT_3,
{GL_FLOAT, 4, GL_FALSE}, // DEC_FLOAT_4,
{GL_BYTE, 4, GL_TRUE}, // DEC_S8_3,
{GL_SHORT, 4, GL_TRUE},// DEC_S16_3,
{GL_UNSIGNED_BYTE, 1, GL_TRUE},// DEC_U8_1,
{GL_UNSIGNED_BYTE, 2, GL_TRUE},// DEC_U8_2,
{GL_UNSIGNED_BYTE, 3, GL_TRUE},// DEC_U8_3,
{GL_UNSIGNED_BYTE, 4, GL_TRUE},// DEC_U8_4,
{GL_UNSIGNED_SHORT, 1, GL_TRUE},// DEC_U16_1,
{GL_UNSIGNED_SHORT, 2, GL_TRUE},// DEC_U16_2,
{GL_UNSIGNED_SHORT, 3, GL_TRUE},// DEC_U16_3,
{GL_UNSIGNED_SHORT, 4, GL_TRUE},// DEC_U16_4,
};
static inline void VertexAttribSetup(int attrib, int fmt, int offset, std::vector<GLRInputLayout::Entry> &entries) {
if (fmt) {
const GlTypeInfo &type = GLComp[fmt];
GLRInputLayout::Entry entry;
entry.offset = offset;
entry.location = attrib;
entry.normalized = type.normalized;
entry.type = type.type;
entry.count = type.count;
entries.push_back(entry);
}
}
// TODO: Use VBO and get rid of the vertexData pointers - with that, we will supply only offsets
GLRInputLayout *DrawEngineGLES::SetupDecFmtForDraw(const DecVtxFormat &decFmt) {
uint32_t key = decFmt.id;
GLRInputLayout *inputLayout;
if (inputLayoutMap_.Get(key, &inputLayout)) {
return inputLayout;
}
std::vector<GLRInputLayout::Entry> entries;
VertexAttribSetup(ATTR_W1, decFmt.w0fmt, decFmt.w0off, entries);
VertexAttribSetup(ATTR_W2, decFmt.w1fmt, decFmt.w1off, entries);
VertexAttribSetup(ATTR_TEXCOORD, decFmt.uvfmt, decFmt.uvoff, entries);
VertexAttribSetup(ATTR_COLOR0, decFmt.c0fmt, decFmt.c0off, entries);
VertexAttribSetup(ATTR_COLOR1, decFmt.c1fmt, decFmt.c1off, entries);
VertexAttribSetup(ATTR_NORMAL, decFmt.nrmfmt, decFmt.nrmoff, entries);
VertexAttribSetup(ATTR_POSITION, DecVtxFormat::PosFmt(), decFmt.posoff, entries);
int stride = decFmt.stride;
inputLayout = render_->CreateInputLayout(entries, stride);
inputLayoutMap_.Insert(key, inputLayout);
return inputLayout;
}
// A new render step means we need to flush any dynamic state. Really, any state that is reset in
// GLQueueRunner::PerformRenderPass.
void DrawEngineGLES::Invalidate(InvalidationCallbackFlags flags) {
if (flags & InvalidationCallbackFlags::RENDER_PASS_STATE) {
// Dirty everything that has dynamic state that will need re-recording.
gstate_c.Dirty(DIRTY_VIEWPORTSCISSOR_STATE | DIRTY_DEPTHSTENCIL_STATE | DIRTY_BLEND_STATE | DIRTY_RASTER_STATE | DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS);
}
}
void DrawEngineGLES::Flush() {
if (!numDrawVerts_) {
return;
}
PROFILE_THIS_SCOPE("flush");
FrameData &frameData = frameData_[render_->GetCurFrame()];
VShaderID vsid;
if (!render_->IsInRenderPass()) {
// Something went badly wrong. Try to survive by simply skipping the draw, though.
_dbg_assert_msg_(false, "Trying to DoFlush while not in a render pass. This is bad.");
// can't goto bail here, skips too many variable initializations. So let's wipe the most important stuff.
indexGen.Reset();
numDecodedVerts_ = 0;
numDrawVerts_ = 0;
numDrawInds_ = 0;
vertexCountInDrawCalls_ = 0;
decodeVertsCounter_ = 0;
decodeIndsCounter_ = 0;
return;
}
GEPrimitiveType prim = prevPrim_;
bool useHWTransform = CanUseHardwareTransform(prim);
if (clipInfoFlags_ & ClipInfoFlags::Valid) {
if (clipInfoFlags_ & ClipInfoFlags::SoftClipCull) {
useHWTransform = false;
}
}
if (clipInfoFlags_ != lastClipInfoFlags_) {
ClipInfoFlags changed = (ClipInfoFlags)((u32)clipInfoFlags_ ^ (u32)lastClipInfoFlags_);
if (changed & (ClipInfoFlags::DepthClampFragment | ClipInfoFlags::MinMaxZDiscard)) {
gstate_c.Dirty(DIRTY_VERTEXSHADER_STATE | DIRTY_FRAGMENTSHADER_STATE | DIRTY_RASTER_STATE);
}
if (changed & ClipInfoFlags::FlatZ) {
gstate_c.Dirty(DIRTY_TEXTURE_PARAMS);
}
lastClipInfoFlags_ = clipInfoFlags_;
}
if (useHWTransform != lastUseHwTransform_) {
gstate_c.Dirty(DIRTY_VERTEXSHADER_STATE | DIRTY_FRAGMENTSHADER_STATE | DIRTY_RASTER_STATE);
lastUseHwTransform_ = useHWTransform;
}
GLRBuffer *vertexBuffer = nullptr;
GLRBuffer *indexBuffer = nullptr;
uint32_t vertexBufferOffset = 0;
uint32_t indexBufferOffset = 0;
Shader *vshader = shaderManager_->ApplyVertexShader(useHWTransform, dec_->VertexType(), clipInfoFlags_, &vsid);
if (!vshader) {
// Both the requested shader and the software transform fallback failed to compile.
// Not much we can do here, let's skip drawing. Note that the failure is cached, so this
// will keep happening for this shader ID - hence the log, or geometry would just silently
// disappear for the rest of the session.
WARN_LOG_N_TIMES(novshader, 5, Log::G3D, "Skipping draw, no vertex shader");
goto bail;
}
useHWTransform = vshader->UseHWTransform(); // In case shader compilation failed and it fell back.
if (useHWTransform) {
if (lastVType_ & GE_VTYPE_WEIGHT_MASK) {
// If software skinning, we're predecoding into "decoded". So make sure we're done, then push that content.
DecodeVerts(dec_, decoded_);
uint32_t size = numDecodedVerts_ * dec_->GetDecVtxFmt().stride;
u8 *dest = (u8 *)frameData.pushVertex->Allocate(size, 4, &vertexBuffer, &vertexBufferOffset);
memcpy(dest, decoded_, size);
} else {
// Figure out how much pushbuffer space we need to allocate.
int vertsToDecode = ComputeNumVertsToDecode();
u8 *dest = (u8 *)frameData.pushVertex->Allocate(vertsToDecode * dec_->GetDecVtxFmt().stride, 4, &vertexBuffer, &vertexBufferOffset);
DecodeVerts(dec_, dest);
}
int vertexCount;
int maxIndex;
bool useElements;
DecodeVerts(dec_, decoded_);
DecodeIndsAndGetData(&prim, &vertexCount, &maxIndex, &useElements, false);
gpuStats.perFrame.numVertsDrawn += vertexCount;
if (useElements) {
uint32_t esz = sizeof(uint16_t) * vertexCount;
void *dest = frameData.pushIndex->Allocate(esz, 2, &indexBuffer, &indexBufferOffset);
// TODO: When we need to apply an index offset, we can apply it directly when copying the indices here.
// Of course, minding the maximum value of 65535...
memcpy(dest, decIndex_, esz);
}
bool hasColor = (lastVType_ & GE_VTYPE_COL_MASK) != GE_VTYPE_COL_NONE;
if (gstate.isModeThrough()) {
gstate_c.vertexFullAlpha = gstate_c.vertexFullAlpha && (hasColor || gstate.getMaterialAmbientA() == 255);
} else {
gstate_c.vertexFullAlpha = gstate_c.vertexFullAlpha && ((hasColor && (gstate.materialupdate & 1)) || gstate.getMaterialAmbientA() == 255) && (!gstate.isLightingEnabled() || gstate.getAmbientA() == 255);
}
if (gstate_c.IsDirty(DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS) && !gstate.isModeClear() && gstate.isTextureMapEnabled()) {
TextureApplyResult textureResult = textureCache_->ApplyTexture(true);
textureCache_->ApplySampler(textureResult, clipInfoFlags_ & ClipInfoFlags::FlatZ, false);
gstate_c.Clean(DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS);
} else if (gstate.getTextureAddress(0) == (gstate.getFrameBufRawAddress() | 0x04000000)) {
// This catches the case of clearing a texture. (#10957)
gstate_c.Dirty(DIRTY_TEXTURE_IMAGE);
}
// Need to ApplyDrawState after ApplyTexture because depal can launch a render pass and that wrecks the state.
ApplyDrawState(prim);
ApplyDrawStateLate(false, 0);
LinkedShader *program = shaderManager_->ApplyFragmentShader(vsid, vshader, pipelineState_, clipInfoFlags_, false);
if (!program) {
// Failed to link. No program is bound, so drawing would use whatever was bound before.
goto bail;
}
GLRInputLayout *inputLayout = SetupDecFmtForDraw(dec_->GetDecVtxFmt());
if (useElements) {
render_->DrawIndexed(inputLayout,
vertexBuffer, vertexBufferOffset,
indexBuffer, indexBufferOffset,
glprim[prim], vertexCount, GL_UNSIGNED_SHORT);
} else {
render_->Draw(
inputLayout, vertexBuffer, vertexBufferOffset,
glprim[prim], 0, vertexCount);
}
if (useDepthRaster_) {
DepthRasterSubmitRaw(prim, dec_, dec_->VertexType(), vertexCount);
}
} else {
PROFILE_THIS_SCOPE("soft");
DecodeVerts(dec_, decoded_);
int vertexCount = DecodeInds();
bool hasColor = (lastVType_ & GE_VTYPE_COL_MASK) != GE_VTYPE_COL_NONE;
if (gstate.isModeThrough()) {
gstate_c.vertexFullAlpha = gstate_c.vertexFullAlpha && (hasColor || gstate.getMaterialAmbientA() == 255);
} else {
gstate_c.vertexFullAlpha = gstate_c.vertexFullAlpha && ((hasColor && (gstate.materialupdate & 1)) || gstate.getMaterialAmbientA() == 255) && (!gstate.isLightingEnabled() || gstate.getAmbientA() == 255);
}
prim = IndexGenerator::GeneralPrim((GEPrimitiveType)drawInds_[0].prim);
int maxIndex = numDecodedVerts_;
// TODO: Split up into multiple draw calls for GLES 2.0 where you can't guarantee support for more than 0x10000 verts.
if (gl_extensions.IsGLES && !gl_extensions.GLES3) {
constexpr int vertexCountLimit = 0x10000 / 3;
if (vertexCount > vertexCountLimit) {
WARN_LOG_REPORT_ONCE(manyVerts, Log::G3D, "Truncating vertex count from %d to %d", vertexCount, vertexCountLimit);
vertexCount = vertexCountLimit;
}
}
// At this point, rect and line primitives are still preserved as such. So, it's the best time to do software depth raster.
// We could piggyback on the viewport transform below, but it gets complicated since it's different per-backend. Which we really
// should clean up one day...
if (useDepthRaster_) {
DepthRasterPredecoded(prim, decoded_, numDecodedVerts_, dec_, vertexCount);
}
bool textureNeedsApply = false;
TextureApplyResult textureResult;
if (gstate_c.IsDirty(DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS) && !gstate.isModeClear() && gstate.isTextureMapEnabled()) {
gstate_c.Clean(DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS);
gstate_c.dstSquared = false;
textureResult = textureCache_->ApplyTexture(true);
textureNeedsApply = true;
} else if (gstate.getTextureAddress(0) == (gstate.getFrameBufRawAddress() | 0x04000000)) {
// This catches the case of clearing a texture. (#10957)
gstate_c.Dirty(DIRTY_TEXTURE_IMAGE);
}
u16 *inds = decIndex_;
SoftwareTransformResult result{};
SoftwareTransformParams params{};
params.everUsedEqualDepth = everUsedEqualDepth_;
params.decoded = decoded_;
params.transformed = transformed_;
params.transformedExpanded = transformedExpanded_;
params.allowClear = true; // Clear in OpenGL respects scissor rects, so we'll use it.
params.allowSeparateAlphaClear = true;
params.clipInfoFlags = clipInfoFlags_;
const SoftwareTransformAction action = RunSoftwareTransform(params, prim, dec_->VertexType(), dec_->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
if (textureNeedsApply) {
textureCache_->ApplySampler(textureResult, clipInfoFlags_ & ClipInfoFlags::FlatZ, result.pixelMapped);
if (gstate_c.dstSquared) {
gstate_c.Dirty(DIRTY_BLEND_STATE);
}
}
// Need to ApplyDrawState after ApplyTexture because depal can launch a render pass and that wrecks the state.
ApplyDrawState(prim);
ApplyDrawStateLate(result.setStencil, result.stencilValue);
LinkedShader *linked = shaderManager_->ApplyFragmentShader(vsid, vshader, pipelineState_, clipInfoFlags_, result.pixelMapped);
if (!linked) {
// Not much we can do here. Let's skip drawing.
goto bail;
}
if (action == SW_DRAW_INDEXED) {
vertexBufferOffset = (uint32_t)frameData.pushVertex->Push(result.drawBuffer, result.drawVertexCount * sizeof(TransformedVertex), 4, &vertexBuffer);
indexBufferOffset = (uint32_t)frameData.pushIndex->Push(inds, sizeof(uint16_t) * result.drawIndexCount, 2, &indexBuffer);
render_->DrawIndexed(
softwareInputLayout_, vertexBuffer, vertexBufferOffset, indexBuffer, indexBufferOffset,
glprim[prim], result.drawIndexCount, GL_UNSIGNED_SHORT);
gpuStats.perFrame.numVertsDrawn += result.drawIndexCount;
} else if (action == SW_CLEAR) {
u32 clearColor = result.color;
float clearDepth = result.depth;
bool colorMask = gstate.isClearModeColorMask();
bool alphaMask = gstate.isClearModeAlphaMask();
bool depthMask = gstate.isClearModeDepthMask();
GLbitfield target = 0;
// Without this, we will clear RGB when clearing stencil, which breaks games.
uint8_t rgbaMask = (colorMask ? 7 : 0) | (alphaMask ? 8 : 0);
if (colorMask || alphaMask) target |= GL_COLOR_BUFFER_BIT;
if (alphaMask) target |= GL_STENCIL_BUFFER_BIT;
if (depthMask) target |= GL_DEPTH_BUFFER_BIT;
render_->Clear(clearColor, clearDepth, clearColor >> 24, target, rgbaMask, vpAndScissor_.scissorX, vpAndScissor_.scissorY, vpAndScissor_.scissorW, vpAndScissor_.scissorH);
if (gstate_c.Use(GPU_USE_CLEAR_RAM_HACK) && colorMask && (alphaMask || gstate_c.framebufFormat == GE_FORMAT_565)) {
int scissorX1 = gstate.getScissorX1();
int scissorY1 = gstate.getScissorY1();
int scissorX2 = gstate.getScissorX2() + 1;
int scissorY2 = gstate.getScissorY2() + 1;
framebufferManager_->ApplyClearToMemory(scissorX1, scissorY1, scissorX2, scissorY2, clearColor);
}
gstate_c.Dirty(DIRTY_BLEND_STATE); // Make sure the color mask gets re-applied.
}
}
bail:
ResetAfterDrawInline();
framebufferManager_->SetColorUpdated(gstate_c.skipDrawReason);
gpuCommon_->NotifyFlush();
}