Files
ARMSX2/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp
T
Brian Degenhardt e9f8f83669 GS/HW: carry the blend-mix factor in the output alpha without dual-source blend
A blend mix hands the blend unit exactly one number - the alpha factor, on the
PS2's 0..2 scale where 128 is opaque. A second fragment output is the usual way
to carry a value on that scale, but it is not the only one: fixed-function
SRC_ALPHA reads the first output's alpha, and the shader can put the factor
there instead. Two cases make that free.

When the target holds its alpha double-scaled, the alpha the shader would write
IS the factor - tfx computes both as C.a/128 under RTA correction - so scaling
the target is the whole change. Otherwise the substitution is free whenever the
pass writes no alpha at all, because the output alpha is discarded on the way to
the target: a draw whose alpha is masked outright, or one whose alpha write has
moved into a second pass under SPLIT_RGB_ONLY.

Only the plain mix1 shape qualifies. The other mix cases rewrite the second
output's RGB independently of its alpha, so there the two outputs really do
carry different values and no substitution exists.

Without this, a GPU with no dual-source blend emulates the equation in the
shader, which needs a fresh destination read per primitive. With neither a
texture barrier nor a multidraw framebuffer copy available, all it gets is one
snapshot taken before the draw, so every primitive after the first composites
against stale pixels. That is what hollowed out God of War II's menu glyphs on
Mali r44p1, where the whole text is a single draw whose drop-shadow and bright
quads overlap each other 200 times.

Measured against a dual-source GPU rendering the same dump: over the text the
mean per-pixel error falls from 3.351 to 0.109 and the worst pixel from 163 to
25, with the lit-pixel count landing on 3896 against the reference's 3898.
Frame-wide it removes 25k of the 49k differing pixels and introduces 22. It
needs no barriers and no target copies at all, where matching this by refreshing
the snapshot per primitive group cost ~1000 render-pass breaks a frame and two
thirds of the frame rate on device.

No effect where dual-source blending exists: 33 frames across 11 dumps are byte
identical.
2026-08-09 17:49:51 -07:00

4300 lines
151 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "GS/Renderers/OpenGL/GLContext.h"
#include "GS/Renderers/OpenGL/GSDeviceOGL.h"
#include "GS/Renderers/OpenGL/GLState.h"
#include "GS/Renderers/Common/GSFramebufferFetchPolicy.h"
#include "GS/Renderers/Common/GSGPUProfile.h"
#include "GS/GSState.h"
#include "GS/Renderers/Common/GSRenderer.h"
#include "GS/GSGL.h"
#include "GS/GSPerfMon.h"
#include "GS/GSUtil.h"
#include "Host.h"
#include "common/Console.h"
#include "common/Error.h"
#include "common/ScopedGuard.h"
#include "common/StringUtil.h"
#include "imgui.h"
#include "IconsFontAwesome.h"
#ifdef ARMSX2_HAS_LIBRASHADER
// librashader only declares its OpenGL entry points when the consumer opts in. Defined by
// CMake only when the Rust toolchain actually produced the library.
#define LIBRA_RUNTIME_OPENGL
#include "librashader.h"
#endif
#include <cinttypes>
#include <fstream>
#include <sstream>
static constexpr u32 g_vs_pc_index = 4;
static constexpr u32 g_vs_ib_index = 3;
static constexpr u32 g_vs_vb_index = 2;
static constexpr u32 g_vs_cb_index = 1;
static constexpr u32 g_ps_cb_index = 0;
static constexpr u32 VERTEX_BUFFER_SIZE = 32 * 1024 * 1024;
static constexpr u32 INDEX_BUFFER_SIZE = 16 * 1024 * 1024;
static constexpr u32 VERTEX_UNIFORM_BUFFER_SIZE = 8 * 1024 * 1024;
static constexpr u32 FRAGMENT_UNIFORM_BUFFER_SIZE = 8 * 1024 * 1024;
static constexpr u32 VERTEX_PUSH_CONSTANT_BUFFER_SIZE = 1024 * 1024;
static constexpr u32 TEXTURE_UPLOAD_BUFFER_SIZE = 128 * 1024 * 1024;
namespace ReplaceGL
{
static void GLAPIENTRY ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height)
{
glScissor(left, bottom, width, height);
}
static void GLAPIENTRY ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h)
{
glViewport(GLint(x), GLint(y), GLsizei(w), GLsizei(h));
}
static void GLAPIENTRY MemoryBarrierAsTextureBarrier()
{
glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
}
static void GLAPIENTRY TextureBarrier()
{
}
} // namespace ReplaceGL
namespace Emulate_DSA
{
// Texture entry point
static void GLAPIENTRY BindTextureUnit(GLuint unit, GLuint texture)
{
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, texture);
}
static void GLAPIENTRY CreateTexture(GLenum target, GLsizei n, GLuint* textures)
{
glGenTextures(1, textures);
}
static void GLAPIENTRY TextureStorage(
GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height)
{
BindTextureUnit(7, texture);
glTexStorage2D(GL_TEXTURE_2D, levels, internalformat, width, height);
}
static void GLAPIENTRY TextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLenum type, const void* pixels)
{
BindTextureUnit(7, texture);
glTexSubImage2D(GL_TEXTURE_2D, level, xoffset, yoffset, width, height, format, type, pixels);
}
static void GLAPIENTRY CopyTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height)
{
BindTextureUnit(7, texture);
glCopyTexSubImage2D(GL_TEXTURE_2D, level, xoffset, yoffset, x, y, width, height);
}
static void GLAPIENTRY CompressedTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset,
GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data)
{
BindTextureUnit(7, texture);
glCompressedTexSubImage2D(GL_TEXTURE_2D, level, xoffset, yoffset, width, height, format, imageSize, data);
}
static void GLAPIENTRY GetTexureImage(
GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels)
{
BindTextureUnit(7, texture);
glGetTexImage(GL_TEXTURE_2D, level, format, type, pixels);
}
static void GLAPIENTRY TextureParameteri(GLuint texture, GLenum pname, GLint param)
{
BindTextureUnit(7, texture);
glTexParameteri(GL_TEXTURE_2D, pname, param);
}
static void GLAPIENTRY GenerateTextureMipmap(GLuint texture)
{
BindTextureUnit(7, texture);
glGenerateMipmap(GL_TEXTURE_2D);
}
// Misc entry point
static void GLAPIENTRY CreateSamplers(GLsizei n, GLuint* samplers)
{
glGenSamplers(n, samplers);
}
// Replace function pointer to emulate DSA behavior
static void Init()
{
glBindTextureUnit = BindTextureUnit;
glCreateTextures = CreateTexture;
glTextureStorage2D = TextureStorage;
glTextureSubImage2D = TextureSubImage;
glCopyTextureSubImage2D = CopyTextureSubImage;
glCompressedTextureSubImage2D = CompressedTextureSubImage;
glGetTextureImage = GetTexureImage;
glTextureParameteri = TextureParameteri;
glGenerateTextureMipmap = GenerateTextureMipmap;
glCreateSamplers = CreateSamplers;
}
} // namespace Emulate_DSA
GSDeviceOGL::GSDeviceOGL() = default;
GSDeviceOGL::~GSDeviceOGL()
{
pxAssert(!m_gl_context);
}
std::vector<GSAdapterInfo> GSDeviceOGL::GetAdapterInfo()
{
std::vector<GSAdapterInfo> ret;
// If OGL is currently active, query the live device directly.
if (g_gs_device && g_gs_device->GetRenderAPI() == RenderAPI::OpenGL)
{
GSAdapterInfo ai;
const std::string& live_name = static_cast<GSDeviceOGL*>(g_gs_device.get())->m_name;
ai.name = live_name.empty() ? "OpenGL" : live_name;
ai.max_texture_size = g_gs_device->GetMaxTextureSize();
ai.max_upscale_multiplier = GSGetMaxUpscaleMultiplier(ai.max_texture_size);
ret.emplace_back(std::move(ai));
return ret;
}
static std::optional<GSAdapterInfo> s_cached_adapter;
if (s_cached_adapter.has_value())
{
ret.emplace_back(*s_cached_adapter);
return ret;
}
static bool s_logged_probe_failure = false;
WindowInfo wi = {};
wi.type = WindowInfo::Type::Surfaceless;
Error error;
std::unique_ptr<GLContext> context = GLContext::Create(wi, &error);
if (!context)
{
if (!s_logged_probe_failure)
{
Console.WarningFmt("GL: Failed to query GL_MAX_TEXTURE_SIZE for adapter info: {}", error.GetDescription());
s_logged_probe_failure = true;
}
return ret;
}
if (!context->MakeCurrent())
{
if (!s_logged_probe_failure)
{
Console.Warning("GL: Failed to make temporary context current while querying GL_MAX_TEXTURE_SIZE for adapter info.");
s_logged_probe_failure = true;
}
return ret;
}
const ScopedGuard done_current([&context]() { context->DoneCurrent(); });
GLint max_texture_size = 1024;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
GSAdapterInfo ai;
const GLubyte* renderer_name = glGetString(GL_RENDERER);
ai.name = renderer_name ? reinterpret_cast<const char*>(renderer_name) : "OpenGL";
ai.max_texture_size = std::max(static_cast<u32>((max_texture_size > 0) ? max_texture_size : 1024), 1024u);
ai.max_upscale_multiplier = GSGetMaxUpscaleMultiplier(ai.max_texture_size);
s_cached_adapter = ai;
ret.emplace_back(std::move(ai));
return ret;
}
GSTexture* GSDeviceOGL::CreateSurface(GSTexture::Usage usage, int width, int height, int levels, GSTexture::Format format)
{
GL_PUSH("Create surface");
pxAssert(GLAD_GL_ARB_shader_image_load_store || !GSTexture::IsShaderWrite(usage));
return new GSTextureOGL(usage, width, height, levels, format);
}
RenderAPI GSDeviceOGL::GetRenderAPI() const
{
return RenderAPI::OpenGL;
}
bool GSDeviceOGL::HasSurface() const
{
return m_window_info.type != WindowInfo::Type::Surfaceless;
}
void GSDeviceOGL::SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle)
{
m_allow_present_throttle = allow_present_throttle;
if (m_vsync_mode == mode)
return;
m_vsync_mode = mode;
SetSwapInterval();
}
bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
{
if (!GSDevice::Create(vsync_mode, allow_present_throttle))
return false;
// GL is a pain and needs the window super early to create the context.
if (!AcquireWindow(true))
return false;
Error error;
m_gl_context = GLContext::Create(m_window_info, &error);
if (!m_gl_context)
{
Console.ErrorFmt("GL: Failed to create any context: {}", error.GetDescription());
return false;
}
if (!m_gl_context->MakeCurrent())
{
Console.Error("GL: Failed to make context current");
return false;
}
m_is_gles = m_gl_context->IsGLES();
if (!CheckFeatures())
return false;
// Store adapter name currently in use
m_name = reinterpret_cast<const char*>(glGetString(GL_RENDERER));
SetSwapInterval();
// Render a frame as soon as possible to clear out whatever was previously being displayed.
if (m_window_info.type != WindowInfo::Type::Surfaceless)
RenderBlankFrame();
if (!GSConfig.DisableShaderCache)
{
if (!m_shader_cache.Open(m_is_gles))
Console.Warning("GL: Shader cache failed to open.");
}
else
{
Console.WriteLn("GL: Not using shader cache.");
}
// GL-ES init bisect markers (Adreno 650/740 boot crash). The crash is a
// silent driver fault with no bad-shader dump, so the LAST stage printed in
// the emulog before it cuts off pinpoints the faulting phase.
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=objects");
// because of fbo bindings below...
GLState::Clear();
// ****************************************************************
// Debug helper
// ****************************************************************
if (GSConfig.UseDebugDevice)
{
if (!m_is_gles) {
glDebugMessageCallback(DebugMessageCallback, NULL);
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, true);
// Useless info message on Nvidia driver
static constexpr const GLuint ids[] = {0x20004};
glDebugMessageControl(GL_DEBUG_SOURCE_API_ARB, GL_DEBUG_TYPE_OTHER_ARB, GL_DONT_CARE,
std::size(ids), ids, false);
}
else if (GLAD_GL_KHR_debug)
{
glDebugMessageCallback(DebugMessageCallback, nullptr);
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, true);
// Useless info message on Nvidia driver
static constexpr const GLuint ids[] = { 0x20004 };
glDebugMessageControl(GL_DEBUG_SOURCE_API_ARB, GL_DEBUG_TYPE_OTHER_ARB, GL_DONT_CARE, std::size(ids), ids, false);
}
// Uncomment synchronous if you want callstacks which match where the error occurred.
glEnable(GL_DEBUG_OUTPUT);
//glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB);
}
// WARNING it must be done after the control setup (at least on MESA)
GL_PUSH("GSDeviceOGL::Create");
// ****************************************************************
// Various object
// ****************************************************************
{
GL_PUSH("GSDeviceOGL::Various");
glGenFramebuffers(1, &m_fbo);
glGenFramebuffers(1, &m_fbo_read);
glGenFramebuffers(1, &m_fbo_write);
OMSetFBO(m_fbo);
// Always read from the first buffer
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_fbo_read);
glReadBuffer(GL_COLOR_ATTACHMENT0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
}
// ****************************************************************
// Vertex buffer state
// ****************************************************************
{
GL_PUSH("GSDeviceOGL::Vertex Buffer");
glGenVertexArrays(1, &m_vao);
IASetVAO(m_vao);
m_vertex_stream_buffer = GLStreamBuffer::Create(GL_ARRAY_BUFFER, VERTEX_BUFFER_SIZE);
m_index_stream_buffer = GLStreamBuffer::Create(GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE);
m_expand_index_stream_buffer = GLStreamBuffer::Create(GL_ARRAY_BUFFER, INDEX_BUFFER_SIZE);
m_vertex_uniform_stream_buffer = GLStreamBuffer::Create(GL_UNIFORM_BUFFER, VERTEX_UNIFORM_BUFFER_SIZE);
m_fragment_uniform_stream_buffer = GLStreamBuffer::Create(GL_UNIFORM_BUFFER, FRAGMENT_UNIFORM_BUFFER_SIZE);
m_vertex_push_constants_stream_buffer = GLStreamBuffer::Create(GL_UNIFORM_BUFFER, VERTEX_PUSH_CONSTANT_BUFFER_SIZE);
glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &m_uniform_buffer_alignment);
if (!m_vertex_stream_buffer || !m_index_stream_buffer || !m_expand_index_stream_buffer ||
!m_vertex_uniform_stream_buffer || !m_fragment_uniform_stream_buffer || !m_vertex_push_constants_stream_buffer)
{
Host::ReportErrorAsync("GS", "Failed to create vertex/index/uniform streaming buffers");
return false;
}
m_vertex_stream_buffer->Bind();
m_index_stream_buffer->Bind();
// Force UBOs to be uploaded on first use.
std::memset(static_cast<void*>(&m_vs_cb_cache), 0xFF, sizeof(m_vs_cb_cache));
std::memset(static_cast<void*>(&m_ps_cb_cache), 0xFF, sizeof(m_ps_cb_cache));
static_assert(sizeof(GSVertexPT1) == sizeof(GSVertex), "wrong GSVertex size");
for (u32 i = 0; i < 8; i++)
glEnableVertexAttribArray(i);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GSVertexPT1), (const GLvoid*)(0));
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(GSVertexPT1), (const GLvoid*)(16));
glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_FALSE, sizeof(GSVertex), (const GLvoid*)(8));
glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, sizeof(GSVertex), (const GLvoid*)(12));
glVertexAttribIPointer(4, 2, GL_UNSIGNED_SHORT, sizeof(GSVertex), (const GLvoid*)(16));
glVertexAttribIPointer(5, 1, GL_UNSIGNED_INT, sizeof(GSVertex), (const GLvoid*)(20));
glVertexAttribIPointer(6, 2, GL_UNSIGNED_SHORT, sizeof(GSVertex), (const GLvoid*)(24));
glVertexAttribPointer(7, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(GSVertex), (const GLvoid*)(28));
if (m_features.vs_expand)
{
glGenVertexArrays(1, &m_expand_vao);
glBindVertexArray(m_expand_vao);
IASetVAO(m_expand_vao);
// Still need the vertex buffer bound, because uploads happen to GL_ARRAY_BUFFER.
m_vertex_stream_buffer->Bind();
std::unique_ptr<u8[]> expand_data = std::make_unique<u8[]>(EXPAND_BUFFER_SIZE);
GenerateExpansionIndexBuffer(expand_data.get());
glGenBuffers(1, &m_expand_ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_expand_ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, EXPAND_BUFFER_SIZE, expand_data.get(), GL_STATIC_DRAW);
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, g_vs_vb_index, m_vertex_stream_buffer->GetGLBufferId(), 0, VERTEX_BUFFER_SIZE);
}
if (m_features.aa1)
{
glGenVertexArrays(1, &m_dummy_vao);
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, g_vs_ib_index, m_expand_index_stream_buffer->GetGLBufferId(), 0, INDEX_BUFFER_SIZE);
}
VSSetPushConstants(0, 0, true); // Avoid undefined data.
}
// ****************************************************************
// Pre Generate the different sampler object
// ****************************************************************
{
GL_PUSH("GSDeviceOGL::Sampler");
for (u32 key = 0; key < std::size(m_ps_ss); key++)
{
m_ps_ss[key] = CreateSampler(PSSamplerSelector(key));
}
}
// these all share the same vertex shader
const std::optional<std::string> convert_glsl = ReadShaderSource("shaders/opengl/convert.glsl");
if (!convert_glsl.has_value())
{
Host::ReportErrorAsync("GS", "Failed to read shaders/opengl/convert.glsl.");
return false;
}
// ****************************************************************
// convert
// ****************************************************************
{
GL_PUSH("GSDeviceOGL::Convert");
m_convert.vs = GetShaderSource("vs_main", GL_VERTEX_SHADER, *convert_glsl);
m_convert.ps.resize(ShaderConvertSelector::NUM_TOTAL_SHADERS);
for (u32 i = 0; i < ShaderConvertSelector::NUM_TOTAL_SHADERS; i++)
{
const ShaderConvertSelector shader = ShaderConvertSelector::Get(i);
const char* name = shader.EntryPoint();
std::string macro;
macro += fmt::format("#define HAS_BILN {}\n", static_cast<int>(shader.Biln()));
macro += fmt::format("#define HAS_STENCIL_OUTPUT {}\n", static_cast<int>(shader.StencilOutput()));
macro += fmt::format("#define HAS_INTEGER_OUTPUT {}\n", static_cast<int>(shader.IntegerOutputBpp() != 0));
macro += fmt::format("#define HAS_DEPTH_OUTPUT {}\n", static_cast<int>(shader.DepthOutput()));
macro += fmt::format("#define HAS_FLOAT32_INPUT {}\n", static_cast<int>(shader.Float32Input()));
macro += fmt::format("#define HAS_FLOAT32_OUTPUT {}\n", static_cast<int>(shader.Float32Output()));
const std::string ps(GetShaderSource(name, GL_FRAGMENT_SHADER, *convert_glsl, macro));
GLProgram& prog = m_convert.ps[i];
if (!m_shader_cache.GetProgram(&prog, m_convert.vs, ps))
return false;
prog.SetFormattedName("Convert pipeline (%s, mask=%x, depth=%d, biln=%d)",
shader.Name(), shader.Mask(), static_cast<int>(shader.DepthOutput()),
static_cast<int>(shader.Biln()));
if (shader.Shader() == ShaderConvert::RGBA_TO_8I || shader.Shader() == ShaderConvert::RGB5A1_TO_8I)
{
prog.RegisterUniform("SBW");
prog.RegisterUniform("DBW");
prog.RegisterUniform("PSM");
prog.RegisterUniform("ScaleFactor");
}
else if (shader.Shader() == ShaderConvert::YUV)
{
prog.RegisterUniform("EMOD");
}
else if (shader.Shader() == ShaderConvert::CLUT_4 || shader.Shader() == ShaderConvert::CLUT_8)
{
prog.RegisterUniform("offset");
prog.RegisterUniform("scale");
}
else if (shader.Shader() == ShaderConvert::DOWNSAMPLE_COPY)
{
prog.RegisterUniform("ClampMin");
prog.RegisterUniform("DownsampleFactor");
prog.RegisterUniform("Weight");
prog.RegisterUniform("StepMultiplier");
}
}
const PSSamplerSelector point;
m_convert.pt = GetSamplerID(point);
PSSamplerSelector bilinear;
bilinear.biln = true;
m_convert.ln = GetSamplerID(bilinear);
m_convert.dss = new GSDepthStencilOGL();
m_convert.dss_write = new GSDepthStencilOGL();
m_convert.dss_write->EnableDepth();
m_convert.dss_write->SetDepth(GL_ALWAYS, true);
}
// ****************************************************************
// present
// ****************************************************************
{
GL_PUSH("GSDeviceOGL::Present");
// these all share the same vertex shader
const std::optional<std::string> shader = ReadShaderSource("shaders/opengl/present.glsl");
if (!shader.has_value())
{
Host::ReportErrorAsync("GS", "Failed to read shaders/opengl/present.glsl.");
return false;
}
std::string present_vs(GetShaderSource("vs_main", GL_VERTEX_SHADER, *shader));
for (size_t i = 0; i < std::size(m_present); i++)
{
const char* name = ShaderEntryPoint(static_cast<PresentShader>(i));
const std::string ps(GetShaderSource(name, GL_FRAGMENT_SHADER, *shader));
if (!m_shader_cache.GetProgram(&m_present[i], present_vs, ps))
return false;
m_present[i].SetFormattedName("Present pipe %s", name);
// This is a bit disgusting, but it saves allocating a UBO when no shaders currently need it.
m_present[i].RegisterUniform("u_source_rect");
m_present[i].RegisterUniform("u_target_rect");
m_present[i].RegisterUniform("u_source_size");
m_present[i].RegisterUniform("u_target_size");
m_present[i].RegisterUniform("u_target_resolution");
m_present[i].RegisterUniform("u_rcp_target_resolution");
m_present[i].RegisterUniform("u_source_resolution");
m_present[i].RegisterUniform("u_rcp_source_resolution");
m_present[i].RegisterUniform("u_time");
}
}
// ****************************************************************
// merge
// ****************************************************************
{
GL_PUSH("GSDeviceOGL::Merge");
const std::optional<std::string> shader = ReadShaderSource("shaders/opengl/merge.glsl");
if (!shader.has_value())
{
Host::ReportErrorAsync("GS", "Failed to read shaders/opengl/merge.glsl.");
return false;
}
for (size_t i = 0; i < std::size(m_merge_obj.ps); i++)
{
const std::string ps(GetShaderSource(fmt::format("ps_main{}", i), GL_FRAGMENT_SHADER, *shader));
if (!m_shader_cache.GetProgram(&m_merge_obj.ps[i], m_convert.vs, ps))
return false;
m_merge_obj.ps[i].SetFormattedName("Merge pipe %zu", i);
m_merge_obj.ps[i].RegisterUniform("BGColor");
}
}
// ****************************************************************
// interlace
// ****************************************************************
{
GL_PUSH("GSDeviceOGL::Interlace");
const std::optional<std::string> shader = ReadShaderSource("shaders/opengl/interlace.glsl");
if (!shader.has_value())
{
Host::ReportErrorAsync("GS", "Failed to read shaders/opengl/interlace.glsl.");
return false;
}
for (size_t i = 0; i < std::size(m_interlace.ps); i++)
{
const std::string ps(GetShaderSource(fmt::format("ps_main{}", i), GL_FRAGMENT_SHADER, *shader));
if (!m_shader_cache.GetProgram(&m_interlace.ps[i], m_convert.vs, ps))
return false;
m_interlace.ps[i].SetFormattedName("Merge pipe %zu", i);
m_interlace.ps[i].RegisterUniform("ZrH");
}
}
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=convert_present_merge_interlace_done");
// ****************************************************************
// Post processing
// ****************************************************************
if (!CompileShadeBoostProgram() || !CompileFXAAProgram())
return false;
// Image load store and GLSL 420pack is core in GL4.2, no need to check.
// NOTE: CAS uses a desktop-GL compute shader (cas.glsl is "#version 420" +
// "#extension GL_ARB_compute_shader") — that source is invalid GLSL ES. On
// devices whose driver only gives a GL ES 3.2 context (e.g. Adreno 650 on the
// Retroid Pocket Mini, where the desktop 4.2 context request fails and we fall
// back to ES), feeding the ES compiler that shader hard-crashes the driver
// during GS init. So gate CAS to a real desktop-GL 4.2 context only; ES devices
// just go without the sharpening filter (TV/CRT present shaders are unaffected —
// they use the ES-aware "#version 320 es" header and compile fine).
m_features.cas_sharpening = (GLAD_GL_VERSION_4_2 && GLAD_GL_ARB_compute_shader) && CreateCASPrograms();
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=postproc_done");
// ****************************************************************
// rasterization configuration
// ****************************************************************
{
GL_PUSH("GSDeviceOGL::Rasterization");
if (!m_is_gles) {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glDisable(GL_MULTISAMPLE);
}
glDisable(GL_CULL_FACE);
glEnable(GL_SCISSOR_TEST);
glDisable(GL_DITHER); // Honestly I don't know!
// Initialise stencil ref/mask.
glStencilFunc(GLState::stencil_func, 1, 1);
}
// ****************************************************************
// DATE
// ****************************************************************
{
GL_PUSH("GSDeviceOGL::Date");
m_date.dss = new GSDepthStencilOGL();
m_date.dss->EnableStencil();
m_date.dss->SetStencil(GL_ALWAYS, GL_REPLACE);
for (size_t i = 0; i < std::size(m_date.primid_ps); i++)
{
const std::string ps(GetShaderSource(
fmt::format("ps_primid_image_init_{}", i),
GL_FRAGMENT_SHADER, *convert_glsl));
m_shader_cache.GetProgram(&m_date.primid_ps[i], m_convert.vs, ps);
m_date.primid_ps[i].SetFormattedName("PrimID Destination Alpha Init %d", i);
}
}
// ****************************************************************
// Use DX coordinate convention
// ****************************************************************
// VS gl_position.z => [-1,-1]
// FS depth => [0, 1]
// because of -1 we loose lot of precision for small GS value
// This extension allow FS depth to range from -1 to 1. So
// gl_position.z could range from [0, 1]
// Change depth convention
if (GLAD_GL_ARB_clip_control)
glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
else if (m_is_gles && GLAD_GL_EXT_clip_control)
// GLES has no ARB_clip_control; GL_EXT_clip_control (advertised by Adreno, and
// some Mali) is the same API/enums. Without it, GLES uses the legacy z-remap that
// CLAMPS PS2 Z >= 2^24 to the far plane (tfx_vgs.glsl), collapsing far depth so
// large-Z world geometry z-fights/vanishes — e.g. God of War II's transparent
// walls. Pairs with HAS_CLIP_CONTROL below (same condition) so the shader matches.
glClipControlEXT(GL_LOWER_LEFT_EXT, GL_ZERO_TO_ONE_EXT);
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=date_raster_done");
// ****************************************************************
// HW renderer shader
// ****************************************************************
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=texturefx_begin");
if (!CreateTextureFX())
return false;
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=texturefx_done");
// ****************************************************************
// Pbo Pool allocation
// ****************************************************************
if (!m_bugs.buggy_pbo)
{
m_texture_upload_buffer = GLStreamBuffer::Create(GL_PIXEL_UNPACK_BUFFER, TEXTURE_UPLOAD_BUFFER_SIZE);
if (m_texture_upload_buffer)
{
// Don't keep it bound, we'll re-bind when we need it.
// Otherwise non-PBO texture uploads break. Yay for global state.
m_texture_upload_buffer->Unbind();
}
else
{
Console.Error("GL: Failed to create texture upload buffer. Using slow path.");
}
}
if (!CreateImGuiProgram())
return false;
// GLES has no pipeline-statistics queries; this extension is desktop-GL only,
// so on Android this stays false and the OSD line shows 0 / degrades gracefully.
m_gpu_pipeline_statistics_supported = (GLAD_GL_ARB_pipeline_statistics_query != 0);
// Basic to ensure structures are correctly packed
static_assert(sizeof(VSSelector) == 1, "Wrong VSSelector size");
static_assert(sizeof(PSSelector) == 16, "Wrong PSSelector size");
static_assert(sizeof(PSSamplerSelector) == 1, "Wrong PSSamplerSelector size");
static_assert(sizeof(OMDepthStencilSelector) == 1, "Wrong OMDepthStencilSelector size");
static_assert(sizeof(OMColorMaskSelector) == 1, "Wrong OMColorMaskSelector size");
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=create_done");
return true;
}
void GSDeviceOGL::Destroy()
{
// Frees GL objects, so it has to run before the context is dropped below.
DestroyShaderChain();
GSDevice::Destroy();
if (m_gl_context)
{
DestroyTimestampQueries();
DestroyPipelineStatisticsQueries();
DestroyResources();
m_gl_context->DoneCurrent();
m_gl_context.reset();
}
}
bool GSDeviceOGL::CreateTextureFX()
{
GL_PUSH("GSDeviceOGL::CreateTextureFX");
std::optional<std::string> vertex_shader = ReadShaderSource("shaders/opengl/tfx_vgs.glsl");
std::optional<std::string> fragment_shader = ReadShaderSource("shaders/opengl/tfx_fs.glsl");
if (!vertex_shader.has_value() || !fragment_shader.has_value())
{
Host::ReportErrorAsync("GS", "Failed to read shaders/opengl/tfx_{vgs,fs}.glsl.");
return false;
}
m_shader_tfx_vgs = std::move(*vertex_shader);
m_shader_tfx_fs = std::move(*fragment_shader);
// warning 1 sampler by image unit. So you cannot reuse m_ps_ss...
m_palette_ss = CreateSampler(PSSamplerSelector(0));
glBindSampler(1, m_palette_ss);
// Enable all bits for stencil operations. Technically 1 bit is
// enough but buffer is polluted with noise. Clear will be limited
// to the mask.
glStencilMask(0xFF);
for (u32 key = 0; key < std::size(m_om_dss); key++)
{
m_om_dss[key] = CreateDepthStencil(OMDepthStencilSelector(key));
}
GLProgram::ResetLastProgram();
return true;
}
bool GSDeviceOGL::CheckFeatures()
{
//bool vendor_id_amd = false;
//bool vendor_id_nvidia = false;
//bool vendor_id_intel = false;
memset(&m_bugs, 0, sizeof(m_bugs));
bool vendor_id_mali = false;
bool vendor_id_adreno = false;
bool vendor_id_apple = false;
const char* vendor_raw = (const char*)glGetString(GL_VENDOR);
const char* renderer_raw = (const char*)glGetString(GL_RENDERER);
const char* gl_version_raw = (const char*)glGetString(GL_VERSION);
const char* vendor_str = vendor_raw ? vendor_raw : "";
const char* renderer_str = renderer_raw ? renderer_raw : "";
// GL_VERSION is the only place the GLES stack names its own build ("OpenGL ES 3.2 v1.r44p1-...",
// "... V@0502", "... build 1.9@4850625"), so it is what the driver-profile resolver parses.
const char* gl_version_str = gl_version_raw ? gl_version_raw : "";
if (std::strstr(vendor_str, "Advanced Micro Devices") || std::strstr(vendor_str, "ATI Technologies Inc.") ||
std::strstr(vendor_str, "ATI"))
{
Console.WriteLn(Color_StrongRed, "GL: AMD GPU detected.");
//vendor_id_amd = true;
}
else if (std::strstr(vendor_str, "NVIDIA Corporation"))
{
Console.WriteLn(Color_StrongGreen, "GL: NVIDIA GPU detected.");
//vendor_id_nvidia = true;
m_bugs.broken_blend_coherency = true;
}
else if (std::strstr(vendor_str, "Intel"))
{
Console.WriteLn(Color_StrongBlue, "GL: Intel GPU detected.");
//vendor_id_intel = true;
}
else if (std::strstr(vendor_str, "ARM") || std::strstr(renderer_str, "Mali"))
{
Console.WriteLn(Color_Yellow, "GL: ARM Mali GPU detected.");
vendor_id_mali = true;
}
else if (std::strstr(vendor_str, "Qualcomm") || std::strstr(renderer_str, "Adreno"))
{
Console.WriteLn(Color_Cyan, "GL: Qualcomm Adreno GPU detected.");
vendor_id_adreno = true;
}
// Matched on the renderer, not the vendor: Apple silicon reports the vendor of whoever
// wrote the driver ("Mesa" under Asahi, "Apple Inc." on macOS), while an Intel Mac reports
// vendor "Apple Inc." with an AMD or Intel GPU. The renderer names the actual GPU.
//
// Apple silicon is a TBDR, but it is not a mobile-vendor part and must not inherit their
// workarounds — detected explicitly so it resolves to its own profile instead of falling
// through to the old not-Mali-therefore-Adreno guess.
else if (std::strstr(renderer_str, "Apple"))
{
Console.WriteLn(Color_StrongCyan, "GL: Apple GPU detected.");
vendor_id_apple = true;
}
#if defined(__ANDROID__)
// ANGLE (GLES-on-Vulkan) reports the underlying GPU in GL_RENDERER, e.g.
// "ANGLE (ARM, Vulkan 1.1.177 (Mali-G77 MC9 ...))", so the native Mali profile and its vendor
// extensions (GL_ARM_shader_framebuffer_fetch, tile optimisations) engage through ANGLE too —
// and that is desirable: they are a large performance win on Mali (dropping them measurably
// regressed FPS on Mali-G615). The Mali-G77 crash under ANGLE was NOT these hacks but stale
// program binaries from the native driver being fed to ANGLE's glProgramBinary(); that is
// fixed at the source in GLShaderCache (driver-keyed cache), so no per-GPU profile gating here.
//
// The driver context below feeds the driver-bug database (ported from EmuCoreX/sashkinbro with
// his approval). GL has no equivalent of VkPhysicalDeviceDriverProperties, so the renderer and
// version strings are all the identity there is; the resolver parses the vendor-specific build
// tag out of them. Nothing here changes behaviour on its own — every workaround it can turn on
// is off unless a rule matches this exact driver.
MobileDriverContext driver_context;
driver_context.api = MobileGpuApi::OpenGL;
driver_context.driver_name = renderer_str;
driver_context.api_version_string = gl_version_str;
const GpuProfileSelection profile_selection = GpuProfileDetector::Resolve(
GSConfig.AndroidGpuProfileOverride, vendor_str, renderer_str, driver_context);
SetRuntimeGPUProfile(profile_selection.runtime_profile);
SetMobileGPUIdentity(profile_selection.gpu);
SetMobileGSTuning(profile_selection.gs_tuning);
SetMobileDriverProfile(profile_selection.driver);
SetMediaTekSoC(profile_selection.is_mediatek_soc);
Console.WriteLn("GL: GPU profile override='%s' resolved='%s' driver='%s' version=%u.%u.%u.%u "
"rules=%u bugs=%016llx workarounds=%016llx.",
GpuProfileDetector::OverrideToConfigString(profile_selection.override_mode),
GpuProfileDetector::RuntimeProfileToString(profile_selection.runtime_profile),
GpuProfileDetector::DriverToString(profile_selection.driver.driver),
static_cast<unsigned>(profile_selection.driver.version.major),
static_cast<unsigned>(profile_selection.driver.version.minor),
static_cast<unsigned>(profile_selection.driver.version.patch),
static_cast<unsigned>(profile_selection.driver.version.build),
static_cast<unsigned>(profile_selection.driver.matched_rule_count),
static_cast<unsigned long long>(profile_selection.driver.bugs),
static_cast<unsigned long long>(profile_selection.driver.workarounds));
DevCon.WriteLn("GL: GPU profile hints: %s", profile_selection.hints.c_str());
bool use_mali_profile = IsMaliGPUProfile();
bool use_adreno_profile = IsAdrenoGPUProfile();
bool use_powervr_profile = IsPowerVRGPUProfile();
#else
// ★ Was `vendor_id_mali ? Mali : Adreno`, which claimed ADRENO for every non-Mali desktop GPU —
// NVIDIA, AMD, Intel and Apple Silicon all identified as Adreno. The locals below were already
// correct (real per-vendor detection), so only the member misfired, which is why it hid: it
// surfaced as Adreno-only workarounds engaging on an M2 (reported by bmd: "GL: Adreno - routing
// depth feedback through the depth sampler"). Mirror the locals instead of guessing, and fall
// back to Unknown — desktop GPUs are not tilers and want none of the mobile vendor paths.
SetRuntimeGPUProfile(vendor_id_mali ? RuntimeGpuProfile::Mali :
vendor_id_adreno ? RuntimeGpuProfile::Adreno :
vendor_id_apple ? RuntimeGpuProfile::Apple :
RuntimeGpuProfile::Unknown);
bool use_mali_profile = vendor_id_mali;
bool use_adreno_profile = vendor_id_adreno;
bool use_powervr_profile = false;
#endif
GLint major_gl = 0;
GLint minor_gl = 0;
glGetIntegerv(GL_MAJOR_VERSION, &major_gl);
glGetIntegerv(GL_MINOR_VERSION, &minor_gl);
if (!m_is_gles && !GLAD_GL_VERSION_3_3)
{
Host::ReportErrorAsync(
"GS", fmt::format(TRANSLATE_FS("GSDeviceOGL", "OpenGL renderer is not supported. Only OpenGL {}.{}\n was found"), major_gl, minor_gl));
return false;
}
// Log extension string for debugging purposes.
Console.WriteLn(fmt::format("GL_VENDOR: {}", reinterpret_cast<const char*>(glGetString(GL_VENDOR))));
Console.WriteLn(fmt::format("GL_VERSION: {}", gl_version_str));
Console.WriteLn(fmt::format("GL_RENDERER: {}", reinterpret_cast<const char*>(glGetString(GL_RENDERER))));
Console.WriteLn(fmt::format(
"GL_SHADING_LANGUAGE_VERSION: {}", reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION))));
std::string extensions = "GL_EXTENSIONS:";
GLint num_extensions = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &num_extensions);
for (GLint i = 0; i < num_extensions; i++)
{
const char* ext = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, i));
if (ext)
{
extensions += ' ';
extensions.append(ext);
}
}
DevCon.WriteLn(std::move(extensions));
if (!m_is_gles) {
if (!GLAD_GL_ARB_shading_language_420pack)
{
Host::ReportFormattedErrorAsync(
"GS", "GL_ARB_shading_language_420pack is not supported, this is required for the OpenGL renderer.");
return false;
}
if (!GLAD_GL_VERSION_4_3 && !GLAD_GL_ARB_copy_image && !GLAD_GL_EXT_copy_image && !GLAD_GL_NV_copy_image)
{
Host::AddOSDMessage(
"GL_ARB_copy_image is not supported, copies will be slower.", Host::OSD_ERROR_DURATION);
}
if (!GLAD_GL_VERSION_4_5 && !GLAD_GL_ARB_clip_control &&
!(m_is_gles && GLAD_GL_EXT_clip_control))
{
Host::AddOSDMessage(
"GL_ARB_clip_control is not supported, depth will be less accurate.", Host::OSD_ERROR_DURATION);
}
}
if (!GLAD_GL_ARB_viewport_array)
{
glScissorIndexed = ReplaceGL::ScissorIndexed;
glViewportIndexedf = ReplaceGL::ViewportIndexedf;
Console.Warning("GL_ARB_viewport_array is not supported! Function pointer will be replaced.");
}
if (!GLAD_GL_ARB_texture_barrier)
{
// First try NV only barrier.
// If that doesn't work then switch to multidraw fb copy.
if (GLAD_GL_NV_texture_barrier)
glTextureBarrier = glTextureBarrierNV;
else
{
glTextureBarrier = ReplaceGL::TextureBarrier;
m_features.multidraw_fb_copy = true;
/* Host::AddOSDMessage(
"GL_ARB_texture_barrier is not supported, blending will be slower.", Host::OSD_ERROR_DURATION);*/
}
}
if (!GLAD_GL_ARB_direct_state_access)
{
Console.Warning("GL_ARB_direct_state_access is not supported, this will reduce performance.");
Emulate_DSA::Init();
}
// glDrawElementsBaseVertex entered core in GL ES 3.2. ANGLE-over-Vulkan caps its context at
// ES 3.1, so GLAD leaves the UNSUFFIXED core pointer NULL even though ANGLE advertises
// GL_OES/EXT_draw_elements_base_vertex (whose suffixed entry points ARE loaded). Every draw
// path calls the unsuffixed glDrawElementsBaseVertex directly (RenderImGui + DrawIndexedPrimitive),
// so on ANGLE the first overlay draw jumped to a null pointer and aborted via the fastmem SIGSEGV
// handler (Mali-G77 / Retroid Pocket 4 Pro). Native Mali/Adreno report ES 3.2 so the core pointer
// is non-null there — which is why this only bit ANGLE. Alias it to the OES/EXT variant, matching
// the glTextureBarrier = glTextureBarrierNV replacement above. Fixes all call sites at once.
if (!glDrawElementsBaseVertex)
{
if (GLAD_GL_OES_draw_elements_base_vertex && glDrawElementsBaseVertexOES)
glDrawElementsBaseVertex = glDrawElementsBaseVertexOES;
else if (GLAD_GL_EXT_draw_elements_base_vertex && glDrawElementsBaseVertexEXT)
glDrawElementsBaseVertex = glDrawElementsBaseVertexEXT;
else
Console.Error("GL: glDrawElementsBaseVertex is unavailable (no core/OES/EXT) — draws will fail.");
}
// glColorMaski (indexed color mask) is core in GLSL ES 3.2 but only extension-provided on
// an ES 3.1 context; GLAD leaves the unsuffixed pointer null on ANGLE (which caps at ES 3.1),
// so GSDeviceOGL::OMSetColorMaskState calls a null pointer → SIGSEGV mid-render (VSync→Merge→
// GetOutput→StretchRect). ANGLE exposes the suffixed variant via GL_OES/EXT_draw_buffers_indexed,
// so alias to it. Same class of fix as glDrawElementsBaseVertex above.
if (!glColorMaski)
{
if (GLAD_GL_OES_draw_buffers_indexed && glColorMaskiOES)
glColorMaski = glColorMaskiOES;
else if (GLAD_GL_EXT_draw_buffers_indexed && glColorMaskiEXT)
glColorMaski = glColorMaskiEXT;
else
Console.Error("GL: glColorMaski is unavailable (no core/OES/EXT) — draws will fail.");
}
// Don't use PBOs when we don't have ARB_buffer_storage, orphaning buffers probably ends up worse than just
// using the normal texture update routines and letting the driver take care of it.
if (!m_is_gles) {
m_bugs.buggy_pbo = !GLAD_GL_VERSION_4_4 && !GLAD_GL_ARB_buffer_storage && !GLAD_GL_EXT_buffer_storage;
} else {
// Mirrors the desktop check: PBOs are useful only when EXT_buffer_storage
// (the GLES port of buffer_storage) is available so we can pin a
// persistent-mapped staging region. Without it the orphaning fallback
// is slower than letting the driver handle the upload directly. The
// previous form here had the test inverted (set buggy=TRUE when the
// extension WAS supported), disabling PBOs on every modern Adreno/Mali
// device. Aligning with the desktop branch's polarity.
m_bugs.buggy_pbo = !GLAD_GL_EXT_buffer_storage;
}
if (m_bugs.buggy_pbo)
Console.Warning("GL: Not using PBOs for texture uploads because buffer_storage is unavailable.");
// Give the user the option to disable PBO usage for downloads.
// Most drivers seem to be faster with PBO.
m_disable_download_pbo = Host::GetBoolSettingValue("EmuCore/GS", "DisableGLDownloadPBO", false);
if (m_disable_download_pbo)
Console.Warning("GL: Not using PBOs for texture downloads, this may reduce performance.");
// optional features based on context
m_features.broken_point_sampler = false;
m_features.primitive_id = true;
// Apple GPUs miscompare depth written from the shader (the PS2 32-bit Z floor) against the
// fixed-function interpolation a later read-only pass tests with, so a GEQUAL retest of the
// same geometry drops out along shared triangle edges and the layer underneath shows through
// as pinpoints -- God of War II's Athena statue, and dark walls in Black. Reproduces here
// identically under GL and Vulkan (748 stray pixels either way), so it is the GPU, not the
// API. See the matching gate in GSDeviceVK::CheckFeatures for the measurements. Mali is
// deliberately not included: the Vulkan path opts it out for early-ZS, but that has not been
// tested on a Mali GL driver.
m_features.no_ps2_z_quantization = GSConfig.DisablePS2DepthQuantization || vendor_id_apple;
// GLES may omit dual-source blending (GL_EXT/ARB_blend_func_extended); desktop GL always has it.
// When absent, GSRendererHW emulates SRC1 blend equations in-shader per-draw rather than forcing
// a global high blending-accuracy level (Mali no longer needs Blending=Max). From sashkinbro/EmuCoreX.
m_features.dual_source_blend =
!m_is_gles || GLAD_GL_EXT_blend_func_extended || GLAD_GL_ARB_blend_func_extended;
// The framebuffer-fetch decision is made ONCE, here, by DecideGLFramebufferFetch (see
// GSFramebufferFetchPolicy.h for why it is a separate pure function). Nothing below may write
// m_features.framebuffer_fetch -- read `fbfetch` instead if you need to know what was decided.
//
// Which drivers cannot survive the in-tile read is a fact about the DRIVER, so it lives in the
// driver-bug database with the rest of them (rule gl-arm-r44p1-attachment-self-read) rather than
// in a substring test here. UseRenderTargetCopyForFeedback is the same workaround the Vulkan
// backend keys its RT-copy fallback on -- fetch and the texture barrier are two spellings of one
// in-tile read, so a driver that fails the read fails both, and one bit answers for both APIs.
//
// This replaced a hand-rolled search for "r44p1" in GL_VERSION. The database matches a PARSED
// driver revision instead, which is what lets a rule say "exactly r44p1" rather than "contains
// r44p1" -- and what would let the next bad blob be a table row. gs_gpu_driver_profile_tests
// pins the real device string through the resolver, because a rule that silently matches
// nothing would put the device straight back on the faulting path with no diagnostic.
const bool fbfetch_driver_blocklisted =
GetMobileDriverProfile().UsesWorkaround(DriverWorkaround::UseRenderTargetCopyForFeedback);
const GSFramebufferFetchDecision fbfetch = DecideGLFramebufferFetch(GLAD_GL_ARM_shader_framebuffer_fetch,
GLAD_GL_EXT_shader_framebuffer_fetch, GLAD_GL_EXT_shader_pixel_local_storage, fbfetch_driver_blocklisted,
GSConfig.DisableFramebufferFetch, use_mali_profile);
m_features.framebuffer_fetch = fbfetch.enabled;
// GL fetch replaces the destination read but does NOT order overlapping primitives within one
// draw, so an overlapping draw keeps its full barrier (see FbFetchDropsDrawBarriers). Stated
// explicitly rather than left to the FeatureSupport memset: Vulkan and Metal both assign this
// bit, and a backend that stays silent reads as an oversight rather than as the answer.
m_features.framebuffer_fetch_orders_overlap = false;
switch (fbfetch.veto)
{
case GSFramebufferFetchVeto::DriverBlocklist:
Console.WriteLn("Mali r44p1: disabling framebuffer fetch (GL context-lost workaround; matches the Vulkan gate).");
break;
case GSFramebufferFetchVeto::UserSetting:
Host::AddOSDMessage(
"Framebuffer fetch was found but is disabled. This will reduce performance.", Host::OSD_ERROR_DURATION);
break;
default:
break;
}
if (GSConfig.OverrideTextureBarriers == 0)
{
m_features.texture_barrier = m_features.framebuffer_fetch; // Force Disabled
m_features.multidraw_fb_copy = false;
Host::AddOSDMessage(
"Texture Barrier is disabled, blending will not be accurate.", Host::OSD_ERROR_DURATION);
}
else if (GSConfig.OverrideTextureBarriers == 1)
{
// No texture barriers supported so try to use memory barrier.
// Not guaranteed to work so only enable it in Force Enabled mode.
if (!GLAD_GL_ARB_texture_barrier && !GLAD_GL_NV_texture_barrier && GLAD_GL_ARB_shader_image_load_store)
glTextureBarrier = ReplaceGL::MemoryBarrierAsTextureBarrier;
m_features.texture_barrier = true; // Force Enabled
m_features.multidraw_fb_copy = false;
}
else
{
m_features.texture_barrier = m_features.framebuffer_fetch || GLAD_GL_ARB_texture_barrier || GLAD_GL_NV_texture_barrier;
// Pick the blend fallback's shape now that we know whether there is a barrier. GLES always
// arrives here with multidraw_fb_copy set (there is no ARB/NV texture barrier), and on a
// device where fetch is also off -- the r44p1 blocklist, the user's setting, or simply no
// fetch extension -- that leaves the per-primitive render-target copy as the blend path,
// which on a tiler means a tile flush and resolve per primitive group. See
// GLUsesPerPrimitiveFbCopy for the measurement; the short version is 0.33 fps.
//
// Only the auto path decides this. Both OverrideTextureBarriers branches above already
// clear the flag themselves, and Force Disabled in particular must keep clearing it on
// desktop too -- the user asked for no barriers, not for a different kind of copy.
m_features.multidraw_fb_copy = GLUsesPerPrimitiveFbCopy(m_features.texture_barrier, m_is_gles);
if (!m_features.texture_barrier && !m_features.multidraw_fb_copy)
{
Console.WriteLn("GL: no texture barrier and no framebuffer fetch — accurate blending reads the "
"render target from a per-draw copy (the per-primitive copy costs a tile flush "
"per primitive on a tiler).");
}
}
m_features.provoking_vertex_last = true;
m_features.dxt_textures = GLAD_GL_EXT_texture_compression_s3tc;
m_features.bptc_textures =
GLAD_GL_VERSION_4_2 || GLAD_GL_ARB_texture_compression_bptc || GLAD_GL_EXT_texture_compression_bptc;
m_features.prefer_new_textures = false;
m_features.stencil_buffer = true;
m_features.test_and_sample_depth = true;
// Auto select chooses depth-as-rt as it appears to be more compatible across hardware.
m_features.depth_feedback = GSConfig.DepthFeedbackMode == GSDepthFeedbackMode::Depth;
if (!m_features.texture_barrier && m_features.multidraw_fb_copy)
{
// Multidraw fb copy can do depth feedback just fine
m_features.depth_feedback |= GSConfig.DepthFeedbackMode == GSDepthFeedbackMode::Auto;
}
// ARMSX2 (Adreno GLES only; inert on every other GPU/profile). Adreno's driver
// rejects a fragment shader declaring TWO framebuffer-fetch `inout` outputs (o_col0
// colour + o_col1 depth), which the depth-as-colour SW-Z path emits for accurate-
// alpha-test draws -> link failure -> garbage (Everybody's Golf 4 / Minna no Golf 4).
// Route depth feedback through the depth path (a single fetch output) so it links, and
// read prior depth via the coherent ARM depth-stencil fetch (gl_LastFragDepthARM) when
// available -- the mode-1 depth sampler read is incoherent on GLES (no barrier on a
// sampled depth attachment) and makes occluded triangles poke through as white shards.
// Only overrides Auto; an explicit DepthFeedbackMode choice is honoured. The GPU
// profile is already resolved above (SetRuntimeGPUProfile), so IsAdrenoGPUProfile()
// is valid here.
if (m_features.framebuffer_fetch && IsAdrenoGPUProfile() &&
GSConfig.DepthFeedbackMode == GSDepthFeedbackMode::Auto)
{
m_features.depth_feedback = true;
m_arm_depth_fetch = GLAD_GL_ARM_shader_framebuffer_fetch_depth_stencil;
Console.WriteLn(m_arm_depth_fetch
? "GL: Adreno - depth feedback via coherent ARM depth-stencil fetch (gl_LastFragDepthARM)."
: "GL: Adreno - routing depth feedback through the depth sampler "
"(avoids the dual framebuffer-fetch output link failure).");
}
// Mobile tile-based GPU profiles. Both Mali and Adreno prefer fresh
// textures over reused ones (avoids tile-flush stalls on partial
// writes), so the texture-pool hint is shared. Mali additionally
// pins framebuffer_fetch to the ARM extension and (on Auto) reuses
// it as the texture-barrier substitute — matched by the
// `#if GPU_PROFILE_MALI` branch in tfx_fs.glsl which picks
// gl_LastFragColorARM. Adreno + Generic fall through to the EXT/PLS
// inout path in the shader's `#else` arm; GPU_PROFILE_ADRENO is
// emitted but not currently consumed by any shader.
if (use_mali_profile || use_adreno_profile || use_powervr_profile)
m_features.prefer_new_textures = true;
#if defined(__ANDROID__)
// Narrows the profile-driven choice above with the per-model tuning; deliberately an
// AND rather than an assignment, so a part whose table entry says "reuse" cannot turn
// preference back on for a profile that did not want it. sashkinbro/EmuCoreX.
m_features.prefer_new_textures &= GetMobileGSTuning().prefer_new_textures;
// See the matching note in GSDeviceVK::CheckFeatures: the forced downgrade of Texture
// Preloading to Partial is gone. It overrode an explicit user setting, applied to every
// unrecognised GPU via the conservative fallback, and depended on device-recreation ordering.
#endif
if (use_mali_profile)
{
// Mali path prefers ARM_shader_framebuffer_fetch (gl_LastFragColorARM) because
// the EXT inout path has been broken across every tested Mali driver. If a
// device was force-overridden to Mali but lacks ARM fbfetch (rare but
// possible), demote to PowerVR profile which uses the same EXT/PLS path the
// catch-all default uses.
//
// ⚠️ This block must NOT re-enable framebuffer fetch, and nothing here may write
// m_features.framebuffer_fetch. It used to set it unconditionally true off the raw
// GLAD_GL_ARM_shader_framebuffer_fetch extension rather than the decision made ~100 lines
// above, which resurrected fetch after both the r44p1 driver guard and the user's
// DisableFramebufferFetch setting -- so on Mali GL there was no way to turn fetch off at
// all. Demotion stays keyed on the extension because that is what it has always meant (a
// Mali profile that cannot reach the ARM shader path is on the wrong profile), but fetch
// being switched off is a blend-path choice, not a reason to change profile.
if (!fbfetch.demote_mali_to_powervr)
{
Console.WriteLn(Color_Yellow, "GL: Applying Mali-specific optimizations for tile-based rendering.");
// texture_barrier already reflects the fetch decision: on GLES the ARB/NV barrier
// extensions are absent, so the Auto branch above resolves to exactly
// framebuffer_fetch. Nothing left to override here -- only to report.
if (m_features.framebuffer_fetch && GSConfig.OverrideTextureBarriers == -1)
Console.WriteLn("GL: Mali optimization - using ARM framebuffer fetch over texture barriers.");
}
else
{
Console.Warning("GL: Mali profile selected but ARM framebuffer fetch is unavailable; demoting to PowerVR/EXT profile.");
SetRuntimeGPUProfile(RuntimeGpuProfile::PowerVR);
use_mali_profile = false;
use_powervr_profile = true;
}
}
if (use_powervr_profile)
{
// PowerVR (Imagination) is tile-based like Mali but ships EXT/PLS fbfetch
// (PLS originated on PowerVR). framebuffer_fetch + texture_barrier values
// from line ~882/908 already reflect the EXT path, so no override needed —
// just confirm the path is wired up.
Console.WriteLn(Color_Yellow, "GL: PowerVR profile active (EXT/PLS framebuffer fetch).");
}
else if (use_adreno_profile)
{
Console.WriteLn(Color_Cyan, "GL: Adreno profile active (EXT/PLS framebuffer fetch).");
// Adreno's GLES driver rejects a fragment shader that declares TWO
// framebuffer-fetch `inout` outputs. The depth-as-colour feedback path
// (DEPTH_FEEDBACK_SUPPORT 2) emits exactly that whenever the colour output
// already needs fetch AND a SW-Z depth draw is in flight -- o_col0 (colour
// fetch) at location 0 and o_col1 (depth fetch) at location 1 both become
// `inout`. That combination is produced by the accurate-alpha-test RGB-only
// + depth-write path, so any game carrying accurateAlphaTest (e.g. Everybody's
// Golf 4 / Minna no Golf 4, SCKA-20057 / SCPS-15059) fails to link those draws
// -> "Output o_col1 location or component exceeds max allowed" -> garbage
// (black-boxed faces, a floating RT rectangle, blue bars). Vulkan is unaffected
// (real depth attachment, no second fetch output). Route depth feedback through
// the real depth sampler (DEPTH_FEEDBACK_SUPPORT 1) so only o_col0 is a fetch
// output and the program links. test_and_sample_depth is already true above,
// and texture_barrier==true here keeps the DS-clone path (bind at ~3402) inert.
// Only override Auto -- an explicit DepthFeedbackMode choice is honoured.
if (m_features.framebuffer_fetch && GSConfig.DepthFeedbackMode == GSDepthFeedbackMode::Auto)
{
m_features.depth_feedback = true;
// The mode-1 depth SAMPLER read is incoherent on GLES (no texture_barrier
// for a sampled depth attachment) -> stale reads make occluded/interior
// triangles poke through as white shards. When the coherent ARM depth-
// stencil fetch extension is present, read prior depth via gl_LastFragDepthARM
// instead (tile-local, one output, no sampler, no feedback-loop bind).
m_arm_depth_fetch = GLAD_GL_ARM_shader_framebuffer_fetch_depth_stencil;
Console.WriteLn(m_arm_depth_fetch
? "GL: Adreno - depth feedback via coherent ARM depth-stencil fetch (gl_LastFragDepthARM)."
: "GL: Adreno - routing depth feedback through the depth sampler "
"(avoids the dual framebuffer-fetch output link failure).");
}
}
{
Console.WriteLn("GL: Framebuffer fetch extension caps: arm=%d ext=%d pls=%d.",
GLAD_GL_ARM_shader_framebuffer_fetch ? 1 : 0, GLAD_GL_EXT_shader_framebuffer_fetch ? 1 : 0,
GLAD_GL_EXT_shader_pixel_local_storage ? 1 : 0);
const char* active_profile_name = use_mali_profile ? "Mali" :
(use_powervr_profile ? "PowerVR" :
(use_adreno_profile ? "Adreno" : "Generic"));
const char* active_fetch_backend =
(fbfetch.backend == GSFramebufferFetchBackend::ARM) ? "ARM" :
((fbfetch.backend == GSFramebufferFetchBackend::EXT) ? "EXT/PLS" : "None");
// The reason rides on the same line as the verdict, deliberately: the resurrection bug
// this policy replaced printed "disabling framebuffer fetch" and "backend: ARM" a tenth of
// a millisecond apart, and neither line said what had decided it.
const char* fetch_veto_reason =
(fbfetch.veto == GSFramebufferFetchVeto::NoExtension) ? " (no fetch extension)" :
((fbfetch.veto == GSFramebufferFetchVeto::DriverBlocklist) ? " (blocked for this driver build)" :
((fbfetch.veto == GSFramebufferFetchVeto::UserSetting) ? " (disabled in settings)" : ""));
Console.WriteLn("GL: Active framebuffer fetch backend (%s profile): %s%s.", active_profile_name,
active_fetch_backend, fetch_veto_reason);
}
if (GLAD_GL_ARB_shader_storage_buffer_object)
{
GLint max_vertex_ssbos = 0;
glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &max_vertex_ssbos);
DevCon.WriteLn("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: %d", max_vertex_ssbos);
m_features.vs_expand = (!GSConfig.DisableVertexShaderExpand && max_vertex_ssbos > 0 && GLAD_GL_ARB_gpu_shader5);
}
if (!m_features.vs_expand)
Console.Warning("GL: Vertex expansion is not supported. This will reduce performance.");
GLint point_range[2] = {};
glGetIntegerv(GL_ALIASED_POINT_SIZE_RANGE, point_range);
m_features.point_expand =
(point_range[0] <= GSConfig.UpscaleMultiplier && point_range[1] >= GSConfig.UpscaleMultiplier);
m_features.line_expand = false;
GLint max_texture_size = 1024;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
m_max_texture_size = std::max(1024u, static_cast<u32>(max_texture_size));
Console.WriteLn("GL: Using %s for point expansion, %s for line expansion and %s for sprite expansion.",
m_features.point_expand ? "hardware" : (m_features.vs_expand ? "vertex expanding" : "UNSUPPORTED"),
m_features.line_expand ? "hardware" : (m_features.vs_expand ? "vertex expanding" : "UNSUPPORTED"),
m_features.vs_expand ? "vertex expanding" : "CPU");
if (!GLAD_GL_ARB_conservative_depth)
{
Console.Warning("GLAD_GL_ARB_conservative_depth is not supported. This will reduce performance.");
}
m_features.aa1 = GSConfig.HWAA1 && m_features.vs_expand && m_features.feedback_loops();
return true;
}
void GSDeviceOGL::SetSwapInterval()
{
if (m_window_info.type == WindowInfo::Type::Surfaceless)
return;
// OpenGL does not support mailbox, only effectively FIFO.
// Fall back to manual throttling in this case.
m_vsync_mode = (m_vsync_mode == GSVSyncMode::Mailbox) ? GSVSyncMode::FIFO : m_vsync_mode;
// Window framebuffer has to be bound to call SetSwapInterval.
s32 interval = static_cast<s32>(m_vsync_mode == GSVSyncMode::FIFO);
// ARM Mali GLES breaks with eglSwapInterval(0): after a handful of swaps the surface
// stops presenting entirely (frozen screen). Never pass 0 on Mali — force interval 1.
// A forced-on vsync beats a frozen display, and the FIFO present pacer still lets
// fast-forward exceed the panel rate via dropped presents. (cf. Dolphin BUG_BROKEN_VSYNC)
if (interval == 0 && IsMaliGPUProfile())
interval = 1;
GLint current_fbo = 0;
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &current_fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
if (!m_gl_context->SetSwapInterval(interval))
WARNING_LOG("GL: Failed to set swap interval to {}", interval);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, current_fbo);
}
void GSDeviceOGL::DestroyResources()
{
m_shader_cache.Close();
if (m_palette_ss != 0)
glDeleteSamplers(1, &m_palette_ss);
m_programs.clear();
for (GSDepthStencilOGL* ds : m_om_dss)
delete ds;
if (m_ps_ss[0] != 0)
glDeleteSamplers(std::size(m_ps_ss), m_ps_ss);
m_imgui.ps.Destroy();
if (m_imgui.vao != 0)
glDeleteVertexArrays(1, &m_imgui.vao);
m_cas.upscale_ps.Destroy();
m_cas.sharpen_ps.Destroy();
m_shadeboost.ps.Destroy();
for (GLProgram& prog : m_date.primid_ps)
prog.Destroy();
delete m_date.dss;
m_fxaa.ps.Destroy();
for (GLProgram& prog : m_present)
prog.Destroy();
for (auto& prog : m_convert.ps)
prog.Destroy();
delete m_convert.dss;
delete m_convert.dss_write;
for (GLProgram& prog : m_interlace.ps)
prog.Destroy();
for (GLProgram& prog : m_merge_obj.ps)
prog.Destroy();
m_fragment_uniform_stream_buffer.reset();
m_vertex_uniform_stream_buffer.reset();
m_vertex_push_constants_stream_buffer.reset();
glBindVertexArray(0);
// Delete the expand VAO here (not m_expand_ibo, which is a buffer object and is
// correctly freed with glDeleteBuffers below). The old code deleted m_expand_ibo
// as a VAO — a no-op — so m_expand_vao leaked on every device teardown/recreate.
if (m_expand_vao != 0)
glDeleteVertexArrays(1, &m_expand_vao);
if (m_vao != 0)
glDeleteVertexArrays(1, &m_vao);
if (m_dummy_vao != 0)
glDeleteVertexArrays(1, &m_dummy_vao);
m_index_stream_buffer.reset();
m_expand_index_stream_buffer.reset();
m_vertex_stream_buffer.reset();
m_texture_upload_buffer.reset();
if (m_expand_ibo)
glDeleteBuffers(1, &m_expand_ibo);
if (m_fbo != 0)
{
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
if (m_fbo_read != 0)
{
glDeleteFramebuffers(1, &m_fbo_read);
m_fbo_read = 0;
}
if (m_fbo_write != 0)
{
glDeleteFramebuffers(1, &m_fbo_write);
m_fbo_write = 0;
}
GLState::fbo = 0;
}
bool GSDeviceOGL::UpdateWindow()
{
pxAssert(m_gl_context);
DestroySurface();
if (!AcquireWindow(false))
return false;
if (!m_gl_context->ChangeSurface(m_window_info))
{
Console.Error("GL: Failed to change surface");
return false;
}
m_window_info = m_gl_context->GetWindowInfo();
if (m_window_info.type != WindowInfo::Type::Surfaceless)
{
// reset vsync rate, since it (usually) gets lost
SetSwapInterval();
RenderBlankFrame();
}
return true;
}
void GSDeviceOGL::ResizeWindow(u32 new_window_width, u32 new_window_height, float new_window_scale)
{
m_window_info.surface_scale = new_window_scale;
if (m_window_info.type == WindowInfo::Type::Surfaceless ||
(m_window_info.surface_width == new_window_width &&
m_window_info.surface_height == new_window_height))
{
return;
}
m_gl_context->ResizeSurface(new_window_width, new_window_height);
m_window_info = m_gl_context->GetWindowInfo();
}
bool GSDeviceOGL::SupportsExclusiveFullscreen() const
{
return false;
}
void GSDeviceOGL::DestroySurface()
{
m_window_info = {};
if (!m_gl_context->ChangeSurface(m_window_info))
Console.Error("GL: Failed to switch to surfaceless");
}
std::string GSDeviceOGL::GetDriverInfo() const
{
const char* gl_vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
const char* gl_renderer = reinterpret_cast<const char*>(glGetString(GL_RENDERER));
const char* gl_version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
const char* gl_shading_language_version = reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION));
return fmt::format(
"OpenGL Context:\n{}\n{} {}\nGLSL: {}", gl_version, gl_vendor, gl_renderer, gl_shading_language_version);
}
GSDevice::PresentResult GSDeviceOGL::DoBeginPresent(bool frame_skip)
{
if (frame_skip || m_window_info.type == WindowInfo::Type::Surfaceless)
return PresentResult::FrameSkipped;
// Get the pipeline statistics for this frame before postprocessing.
if (m_gpu_pipeline_statistics_enabled)
PopPipelineStatisticsQuery();
OMSetFBO(0);
OMSetColorMaskState();
// On TBDR, hint that the default framebuffer's prior content is throwaway
// before the tile is loaded for the present quad. The color attachment is
// fully overwritten by the clear+blit below, and depth/stencil are never
// used at all on the system framebuffer. Default-FBO uses GL_COLOR / DEPTH
// / STENCIL (not GL_*_ATTACHMENT). Pure TBDR tile-bandwidth win and inert on
// desktop immediate renderers, so gated to GLES to keep the desktop path canonical.
if (m_is_gles)
{
const GLenum attachments[] = {GL_COLOR, GL_DEPTH, GL_STENCIL};
glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(attachments), attachments);
}
glDisable(GL_SCISSOR_TEST);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_SCISSOR_TEST);
const GSVector2i size = GetWindowSize();
SetViewport(size);
SetScissor(GSVector4i::loadh(size));
return PresentResult::OK;
}
void GSDeviceOGL::EndPresent()
{
RenderImGui();
if (m_gpu_timing_enabled)
PopTimestampQuery();
// Discard the default framebuffer's depth/stencil before the swap. We
// never wrote anything meaningful to them, so on TBDR drivers writing
// the tile back to system memory at SwapBuffers is wasted bandwidth.
// Color is preserved (it's what gets presented). GLES/TBDR-only (inert on desktop).
if (m_is_gles)
{
const GLenum attachments[] = {GL_DEPTH, GL_STENCIL};
glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(attachments), attachments);
}
m_gl_context->SwapBuffers();
if (m_gpu_timing_enabled)
KickTimestampQuery();
if (m_gpu_pipeline_statistics_enabled)
KickPipelineStatisticsQuery();
}
void GSDeviceOGL::CreateTimestampQueries()
{
glGenQueries(static_cast<u32>(m_timestamp_queries.size()), m_timestamp_queries.data());
KickTimestampQuery();
}
void GSDeviceOGL::DestroyTimestampQueries()
{
if (m_timestamp_queries[0] == 0)
return;
if (m_timestamp_query_started)
glEndQuery(GL_TIME_ELAPSED);
glDeleteQueries(static_cast<u32>(m_timestamp_queries.size()), m_timestamp_queries.data());
m_timestamp_queries.fill(0);
m_read_timestamp_query = 0;
m_write_timestamp_query = 0;
m_waiting_timestamp_queries = 0;
m_timestamp_query_started = false;
}
void GSDeviceOGL::PopTimestampQuery()
{
while (m_waiting_timestamp_queries > 0)
{
#if defined(__ANDROID__)
// GLES doesn't expose glGetQueryObjectiv / glGetQueryObjectui64v; both
// availability and result use the u32 form. Caps at ~4.29s of
// nanoseconds — fine for per-frame timing. Provided by the
// EXT_disjoint_timer_query extension (GL_TIME_ELAPSED_EXT === 0x88BF
// === GL_TIME_ELAPSED here).
//
// Prior version of this branch was broken: it called glBeginQuery on
// the read slot then immediately tried to read its result (always 0,
// query never ended) and incremented m_waiting_timestamp_queries
// instead of decrementing — accumulator stayed stuck at 0 in HW
// renderer OSD ("GPU: 0%" symptom).
GLuint available = 0;
glGetQueryObjectuiv(m_timestamp_queries[m_read_timestamp_query], GL_QUERY_RESULT_AVAILABLE, &available);
if (!available)
break;
GLuint result = 0;
glGetQueryObjectuiv(m_timestamp_queries[m_read_timestamp_query], GL_QUERY_RESULT, &result);
m_accumulated_gpu_time += static_cast<float>(static_cast<double>(result) / 1000000.0);
#else
GLint available = 0;
glGetQueryObjectiv(m_timestamp_queries[m_read_timestamp_query], GL_QUERY_RESULT_AVAILABLE, &available);
if (!available)
break;
u64 result = 0;
glGetQueryObjectui64v(m_timestamp_queries[m_read_timestamp_query], GL_QUERY_RESULT, &result);
m_accumulated_gpu_time += static_cast<float>(static_cast<double>(result) / 1000000.0);
#endif
m_read_timestamp_query = (m_read_timestamp_query + 1) % NUM_TIMESTAMP_QUERIES;
m_waiting_timestamp_queries--;
}
if (m_timestamp_query_started)
{
glEndQuery(GL_TIME_ELAPSED);
m_write_timestamp_query = (m_write_timestamp_query + 1) % NUM_TIMESTAMP_QUERIES;
m_timestamp_query_started = false;
m_waiting_timestamp_queries++;
}
}
void GSDeviceOGL::KickTimestampQuery()
{
if (m_timestamp_query_started || m_waiting_timestamp_queries == NUM_TIMESTAMP_QUERIES)
return;
glBeginQuery(GL_TIME_ELAPSED, m_timestamp_queries[m_write_timestamp_query]);
m_timestamp_query_started = true;
}
bool GSDeviceOGL::SetGPUTimingEnabled(bool enabled)
{
if (m_gpu_timing_enabled == enabled)
return true;
m_gpu_timing_enabled = enabled;
if (m_gpu_timing_enabled)
CreateTimestampQueries();
else
DestroyTimestampQueries();
return true;
}
float GSDeviceOGL::GetAndResetAccumulatedGPUTime()
{
const float value = m_accumulated_gpu_time;
m_accumulated_gpu_time = 0.0f;
return value;
}
// NOTE: These GL pipeline-statistics queries are desktop-GL only. GLES (Android)
// has neither GL_ARB_pipeline_statistics_query nor the glGetQueryObjectiv /
// glGetQueryObjectui64v result readers (see PopTimestampQuery for the same
// GLES gap), so the whole path is compiled out under __ANDROID__. On Android
// m_gpu_pipeline_statistics_supported stays false and these are never invoked;
// the OSD line just shows 0 / degrades to n/a. Real stats come from Vulkan.
void GSDeviceOGL::PopPipelineStatisticsQuery()
{
#if !defined(__ANDROID__)
while (m_waiting_pipeline_statistics_queries > 0)
{
GLint available[2] = {};
glGetQueryObjectiv(m_pipeline_statistics_queries[m_read_pipeline_statistics_query][0], GL_QUERY_RESULT_AVAILABLE, &available[0]);
glGetQueryObjectiv(m_pipeline_statistics_queries[m_read_pipeline_statistics_query][1], GL_QUERY_RESULT_AVAILABLE, &available[1]);
if (!(available[0] && available[1]))
break;
GPUPipelineStatistics stats = {};
glGetQueryObjectui64v(m_pipeline_statistics_queries[m_read_pipeline_statistics_query][0], GL_QUERY_RESULT, &stats.vs_invocations);
glGetQueryObjectui64v(m_pipeline_statistics_queries[m_read_pipeline_statistics_query][1], GL_QUERY_RESULT, &stats.ps_invocations);
m_accumulated_gpu_pipeline_statistics.vs_invocations += stats.vs_invocations;
m_accumulated_gpu_pipeline_statistics.ps_invocations += stats.ps_invocations;
m_read_pipeline_statistics_query = (m_read_pipeline_statistics_query + 1) % NUM_PIPELINE_STATISTICS_QUERIES;
m_waiting_pipeline_statistics_queries--;
}
if (m_pipeline_statistics_query_started)
{
glEndQuery(GL_VERTEX_SHADER_INVOCATIONS_ARB);
glEndQuery(GL_FRAGMENT_SHADER_INVOCATIONS_ARB);
m_write_pipeline_statistics_query = (m_write_pipeline_statistics_query + 1) % NUM_PIPELINE_STATISTICS_QUERIES;
m_pipeline_statistics_query_started = false;
m_waiting_pipeline_statistics_queries++;
}
#endif
}
void GSDeviceOGL::KickPipelineStatisticsQuery()
{
#if !defined(__ANDROID__)
if (m_pipeline_statistics_query_started || m_waiting_pipeline_statistics_queries == NUM_PIPELINE_STATISTICS_QUERIES)
return;
glBeginQuery(GL_VERTEX_SHADER_INVOCATIONS_ARB, m_pipeline_statistics_queries[m_write_pipeline_statistics_query][0]);
glBeginQuery(GL_FRAGMENT_SHADER_INVOCATIONS_ARB, m_pipeline_statistics_queries[m_write_pipeline_statistics_query][1]);
m_pipeline_statistics_query_started = true;
#endif
}
void GSDeviceOGL::CreatePipelineStatisticsQueries()
{
#if !defined(__ANDROID__)
for (int i = 0; i < NUM_PIPELINE_STATISTICS_QUERIES; i++)
{
glGenQueries(2, m_pipeline_statistics_queries[i].data());
}
KickPipelineStatisticsQuery();
#endif
}
void GSDeviceOGL::DestroyPipelineStatisticsQueries()
{
#if !defined(__ANDROID__)
if (m_pipeline_statistics_queries[0][0] == 0)
return;
if (m_pipeline_statistics_query_started)
{
glEndQuery(GL_VERTEX_SHADER_INVOCATIONS_ARB);
glEndQuery(GL_FRAGMENT_SHADER_INVOCATIONS_ARB);
}
for (size_t i = 0; i < m_pipeline_statistics_queries.size(); i++)
{
glDeleteQueries(2, m_pipeline_statistics_queries[i].data());
m_pipeline_statistics_queries[i].fill(0);
}
m_read_pipeline_statistics_query = 0;
m_write_pipeline_statistics_query = 0;
m_waiting_pipeline_statistics_queries = 0;
m_pipeline_statistics_query_started = false;
#endif
}
GPUPipelineStatistics GSDeviceOGL::GetAndResetAccumulatedGPUPipelineStatistics()
{
GPUPipelineStatistics stats = m_accumulated_gpu_pipeline_statistics;
m_accumulated_gpu_pipeline_statistics = {};
return stats;
}
bool GSDeviceOGL::SetGPUPipelineStatisticsEnabled(bool enabled)
{
if (m_gpu_pipeline_statistics_enabled == enabled)
return true;
m_gpu_pipeline_statistics_enabled = enabled && m_gpu_pipeline_statistics_supported;
if (m_gpu_pipeline_statistics_enabled)
CreatePipelineStatisticsQueries();
else
DestroyPipelineStatisticsQueries();
return true;
}
void GSDeviceOGL::DrawPrimitive()
{
g_perfmon.Put(GSPerfMon::DrawCalls, 1);
glDrawArrays(m_draw_topology, m_vertex.start, m_vertex.count);
}
void GSDeviceOGL::DrawIndexedPrimitive()
{
DrawIndexedPrimitive(0, m_index.count);
}
void GSDeviceOGL::DrawIndexedPrimitive(int offset, int count)
{
g_perfmon.Put(GSPerfMon::DrawCalls, 1);
glDrawElementsBaseVertex(m_draw_topology, count, GL_UNSIGNED_SHORT,
reinterpret_cast<void*>((static_cast<u32>(m_index.start) + static_cast<u32>(offset)) * sizeof(u16)),
static_cast<GLint>(m_vertex.start));
}
void GSDeviceOGL::DrawIndexedPrimitiveVSExpand(int offset, int count, bool vs_indexing, int vs_indexing_expansion)
{
g_perfmon.Put(GSPerfMon::DrawCalls, 1);
if (vs_indexing)
{
VSSetPushConstants(m_vertex.start, m_index.start + offset);
glDrawArrays(m_draw_topology, 0, count * vs_indexing_expansion);
}
else
{
VSSetPushConstants(m_vertex.start);
glDrawElementsBaseVertex(m_draw_topology, count, GL_UNSIGNED_SHORT,
reinterpret_cast<void*>((static_cast<u32>(m_index.start) + static_cast<u32>(offset)) * sizeof(u16)), 0);
}
}
void GSDeviceOGL::Draw(const GSHWDrawConfig& config, int offset, int count)
{
if (config.vs.expand != GSHWDrawConfig::VSExpand::None)
{
const bool vs_indexing = config.vs.UseVSExpandIndexBuffer();
const u32 vs_indexing_expansion = GetExpansionFactor(config.vs.expand);
DrawIndexedPrimitiveVSExpand(offset, count, vs_indexing, vs_indexing_expansion);
}
else
{
DrawIndexedPrimitive(offset, count);
}
}
void GSDeviceOGL::Draw(const GSHWDrawConfig& config)
{
Draw(config, 0, m_index.count);
}
void GSDeviceOGL::CommitClear(GSTexture* t, bool use_write_fbo)
{
GSTextureOGL* T = static_cast<GSTextureOGL*>(t);
if (!T->IsRenderTargetOrDepthStencil() || T->GetState() == GSTexture::State::Dirty)
return;
if (use_write_fbo)
{
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo_write);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
t->IsRenderTarget() ? static_cast<GSTextureOGL*>(t)->GetID() : 0, 0);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, m_features.framebuffer_fetch ? GL_DEPTH_ATTACHMENT : GL_DEPTH_STENCIL_ATTACHMENT,
GL_TEXTURE_2D, t->IsDepthStencil() ? static_cast<GSTextureOGL*>(t)->GetID() : 0, 0);
}
else
{
OMSetFBO(m_fbo);
if (T->IsDepthStencil())
{
if (GLState::rt && GLState::rt->GetSize() != T->GetSize())
OMAttachRt(nullptr);
OMAttachDs(T);
}
else
{
if (GLState::ds && GLState::ds->GetSize() != T->GetSize())
OMAttachDs(nullptr);
OMAttachRt(T);
}
}
if (T->GetState() == GSTexture::State::Invalidated)
{
// glInvalidateFramebuffer is core in GL 4.3 and GLES 3.0. The original
// gate skipped GLES, so on Adreno/Mali every "this content is dead"
// hint from the texture cache fell through to a no-op — the tile got
// written back to system memory anyway. On TBDR that's pure waste.
if (GLAD_GL_VERSION_4_3 || m_is_gles)
{
if (T->IsDepthStencil())
{
const GLenum attachments[] = {GL_DEPTH_STENCIL_ATTACHMENT};
glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(attachments), attachments);
}
else
{
const GLenum attachments[] = {GL_COLOR_ATTACHMENT0};
glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(attachments), attachments);
}
}
}
else
{
glDisable(GL_SCISSOR_TEST);
if (T->IsDepthStencil())
{
const float d = T->GetClearDepth();
if (GLState::depth_mask)
{
glClearBufferfv(GL_DEPTH, 0, &d);
}
else
{
glDepthMask(true);
glClearBufferfv(GL_DEPTH, 0, &d);
glDepthMask(false);
}
}
else
{
const u32 old_color_mask = GLState::wrgba;
OMSetColorMaskState();
const GSVector4 c_unorm = T->GetClearForFormat();
if (T->IsIntegerFormat())
{
if (T->IsUnsignedFormat())
glClearBufferuiv(GL_COLOR, 0, c_unorm.U32);
else
glClearBufferiv(GL_COLOR, 0, c_unorm.I32);
}
else
{
glClearBufferfv(GL_COLOR, 0, c_unorm.v);
}
OMSetColorMaskState(OMColorMaskSelector(old_color_mask));
}
glEnable(GL_SCISSOR_TEST);
}
T->SetState(GSTexture::State::Dirty);
if (use_write_fbo)
{
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
t->IsRenderTarget() ?
GL_COLOR_ATTACHMENT0 :
(m_features.framebuffer_fetch ? GL_DEPTH_ATTACHMENT : GL_DEPTH_STENCIL_ATTACHMENT),
GL_TEXTURE_2D, 0, 0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLState::fbo);
}
}
std::unique_ptr<GSDownloadTexture> GSDeviceOGL::CreateDownloadTexture(u32 width, u32 height, GSTexture::Format format)
{
return GSDownloadTextureOGL::Create(width, height, format);
}
GLuint GSDeviceOGL::CreateSampler(PSSamplerSelector sel)
{
GL_PUSH("Create Sampler");
GLuint sampler;
glCreateSamplers(1, &sampler);
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, sel.IsMagFilterLinear() ? GL_LINEAR : GL_NEAREST);
if (!sel.UseMipmapFiltering())
{
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, sel.IsMinFilterLinear() ? GL_LINEAR : GL_NEAREST);
}
else
{
if (sel.IsMipFilterLinear())
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, sel.IsMinFilterLinear() ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST_MIPMAP_LINEAR);
else
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, sel.IsMinFilterLinear() ? GL_LINEAR_MIPMAP_NEAREST : GL_NEAREST_MIPMAP_NEAREST);
}
glSamplerParameterf(sampler, GL_TEXTURE_MIN_LOD, -1000.0f);
glSamplerParameterf(sampler, GL_TEXTURE_MAX_LOD, sel.lodclamp ? 0.25f : 1000.0f);
if (sel.tau)
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_REPEAT);
else
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
if (sel.tav)
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_REPEAT);
else
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
return sampler;
}
GLuint GSDeviceOGL::GetSamplerID(PSSamplerSelector ssel)
{
return m_ps_ss[ssel.key];
}
GSDepthStencilOGL* GSDeviceOGL::CreateDepthStencil(OMDepthStencilSelector dssel)
{
GSDepthStencilOGL* dss = new GSDepthStencilOGL();
if (dssel.date)
{
dss->EnableStencil();
if (dssel.date_one)
dss->SetStencil(GL_EQUAL, GL_ZERO);
else
dss->SetStencil(GL_EQUAL, GL_KEEP);
}
if (dssel.ztst != ZTST_ALWAYS || dssel.zwe)
{
static const GLenum ztst[] =
{
GL_NEVER,
GL_ALWAYS,
GL_GEQUAL,
GL_GREATER
};
dss->EnableDepth();
dss->SetDepth(ztst[dssel.ztst], dssel.zwe);
}
return dss;
}
GSTexture* GSDeviceOGL::InitPrimDateTexture(GSTexture* rt, const GSVector4i& area, SetDATM datm)
{
const GSVector2i& rtsize = rt->GetSize();
GSTexture* tex = CreateRenderTarget(rtsize.x, rtsize.y, GSTexture::Format::PrimID, false);
if (!tex)
return nullptr;
GL_PUSH("PrimID Destination Alpha Clear");
DoStretchRect(rt, GSVector4(area) / GSVector4(rtsize).xyxy(), tex, GSVector4(area), m_date.primid_ps[static_cast<u8>(datm)], Nearest);
return tex;
}
std::string GSDeviceOGL::GetShaderSource(const std::string_view entry, GLenum type, const std::string_view glsl_h_code, const std::string_view macro_sel)
{
std::string src = GenGlslHeader(entry, type, macro_sel);
src += glsl_h_code;
return src;
}
std::string GSDeviceOGL::GenGlslHeader(const std::string_view entry, GLenum type, const std::string_view macro)
{
std::string header;
if (m_is_gles)
{
if (GLAD_GL_ES_VERSION_3_2)
header = "#version 320 es\n";
else if (GLAD_GL_ES_VERSION_3_1)
header = "#version 310 es\n";
// Vertex↔fragment interface blocks (out/in SHADER {}) are core only in GLSL ES
// 3.20. On an ES 3.1 context — notably ANGLE, which reports ES 3.1 and validates
// shaders strictly — they must be enabled explicitly or every TFX shader fails with
// "'out' : invalid qualifier: shader IO blocks need shader io block extension",
// leaving no valid programs (black screen, and downstream crashes when a null
// program is later bound). Native Mali is lenient and compiled these fine without
// it; ANGLE does not. Harmless where already core.
if (!GLAD_GL_ES_VERSION_3_2)
{
if (GLAD_GL_EXT_shader_io_blocks)
header += "#extension GL_EXT_shader_io_blocks : enable\n";
else if (GLAD_GL_OES_shader_io_blocks)
header += "#extension GL_OES_shader_io_blocks : enable\n";
}
if (GLAD_GL_EXT_blend_func_extended)
header += "#extension GL_EXT_blend_func_extended : require\n";
if (GLAD_GL_ARB_blend_func_extended)
header += "#extension GL_ARB_blend_func_extended : require\n";
if (m_features.framebuffer_fetch)
{
if (GLAD_GL_ARM_shader_framebuffer_fetch)
header += "#extension GL_ARM_shader_framebuffer_fetch : require\n";
else if (GLAD_GL_EXT_shader_framebuffer_fetch)
header += "#extension GL_EXT_shader_framebuffer_fetch : require\n";
}
// Coherent prior-depth read for SW-Z feedback (gl_LastFragDepthARM).
if (m_arm_depth_fetch)
header += "#extension GL_ARM_shader_framebuffer_fetch_depth_stencil : require\n";
header += "precision highp float;\n";
header += "precision highp int;\n";
header += "precision highp sampler2D;\n";
if (GLAD_GL_ES_VERSION_3_1)
header += "precision highp sampler2DMS;\n";
if (GLAD_GL_ES_VERSION_3_2)
header += "precision highp usamplerBuffer;\n";
if (!GLAD_GL_EXT_blend_func_extended && !GLAD_GL_ARB_blend_func_extended)
{
if (!GLAD_GL_ARM_shader_framebuffer_fetch)
fprintf(stderr, "Dual source blending is not supported\n");
header += "#define DISABLE_DUAL_SOURCE\n";
}
}
else {
// Intel's GL driver doesn't like the readonly qualifier with 3.3 GLSL.
if (m_features.vs_expand && GLAD_GL_VERSION_4_3)
{
header = "#version 430 core\n";
}
else
{
header = "#version 330 core\n";
header += "#extension GL_ARB_shading_language_420pack : require\n";
if (GLAD_GL_ARB_gpu_shader5)
header += "#extension GL_ARB_gpu_shader5 : require\n";
if (m_features.vs_expand)
header += "#extension GL_ARB_shader_storage_buffer_object: require\n";
}
if (m_features.framebuffer_fetch && GLAD_GL_EXT_shader_framebuffer_fetch)
header += "#extension GL_EXT_shader_framebuffer_fetch : require\n";
}
if (m_features.framebuffer_fetch)
header += "#define HAS_FRAMEBUFFER_FETCH 1\n";
else
header += "#define HAS_FRAMEBUFFER_FETCH 0\n";
header += fmt::format("#define HAS_EXT_SHADER_FRAMEBUFFER_FETCH {}\n", GLAD_GL_EXT_shader_framebuffer_fetch ? 1 : 0);
header += fmt::format("#define HAS_ARM_SHADER_FRAMEBUFFER_FETCH {}\n", GLAD_GL_ARM_shader_framebuffer_fetch ? 1 : 0);
header += fmt::format("#define HAS_ARM_DEPTH_FETCH {}\n", m_arm_depth_fetch ? 1 : 0);
header += fmt::format("#define HAS_EXT_SHADER_PIXEL_LOCAL_STORAGE {}\n", GLAD_GL_EXT_shader_pixel_local_storage ? 1 : 0);
header += fmt::format("#define GPU_PROFILE_MALI {}\n", IsMaliGPUProfile() ? 1 : 0);
header += fmt::format("#define GPU_PROFILE_ADRENO {}\n", IsAdrenoGPUProfile() ? 1 : 0);
header += fmt::format("#define GPU_PROFILE_POWERVR {}\n", IsPowerVRGPUProfile() ? 1 : 0);
header += fmt::format("#define HAS_ARM_DEPTH_FETCH {}\n", m_arm_depth_fetch ? 1 : 0);
// Shader-compiler workarounds from the driver-bug database (ported from EmuCoreX/sashkinbro
// with his approval). Each one is off unless a rule matched this exact driver, so the emitted
// GLSL is byte-identical to before on anything the database does not know about.
//
// ScalarizeVectorBitwiseAnd additionally keeps the pre-existing IsMaliGPUProfile() gate: this
// tree has scalarized vector ANDs on every Mali GL profile since the original fix, including
// Mali reached through ANGLE or Panfrost where the database resolves a non-ARM driver and would
// otherwise match nothing. Widening only, never narrowing — nobody loses a fix they had.
header += fmt::format("#define DRIVER_SCALARIZE_VECTOR_BITWISE_AND {}\n",
(UsesMobileDriverWorkaround(DriverWorkaround::ScalarizeVectorBitwiseAnd) || IsMaliGPUProfile()) ? 1 : 0);
header += fmt::format("#define DRIVER_REWRITE_BOOLEAN_NEGATION {}\n",
UsesMobileDriverWorkaround(DriverWorkaround::RewriteBooleanNegation) ? 1 : 0);
header += fmt::format("#define DRIVER_STORE_BITWISE_NEGATION_IN_TEMPORARY {}\n",
UsesMobileDriverWorkaround(DriverWorkaround::StoreBitwiseNegationInTemporary) ? 1 : 0);
if (GLAD_GL_ARB_conservative_depth)
{
header += "#extension GL_ARB_conservative_depth : enable\n";
header += "#define PS_HAS_CONSERVATIVE_DEPTH 1\n";
}
else
{
header += "#define PS_HAS_CONSERVATIVE_DEPTH 0\n";
}
if (!m_features.texture_barrier && !m_features.multidraw_fb_copy)
{
header += "#define DEPTH_FEEDBACK_SUPPORT 0\n"; // None
}
else if (m_features.depth_feedback)
{
header += "#define DEPTH_FEEDBACK_SUPPORT 1\n"; // Depth
}
else
{
header += "#define DEPTH_FEEDBACK_SUPPORT 2\n"; // Depth as RT
}
// Must match the glClipControl(EXT) enable above: desktop ARB, or GLES with EXT.
if (GLAD_GL_ARB_clip_control || (m_is_gles && GLAD_GL_EXT_clip_control))
header += "#define HAS_CLIP_CONTROL 1\n";
else
header += "#define HAS_CLIP_CONTROL 0\n";
// Allow to puts several shader in 1 files
switch (type)
{
case GL_VERTEX_SHADER:
header += "#define VERTEX_SHADER 1\n";
break;
case GL_GEOMETRY_SHADER:
header += "#define GEOMETRY_SHADER 1\n";
break;
case GL_FRAGMENT_SHADER:
header += "#define FRAGMENT_SHADER 1\n";
break;
default:
pxAssert(0);
}
// Don't remove this, the recursive macro breaks some Intel drivers.
if (entry != "main")
{
// Select the entry point ie the main function
header += "#define ";
header += entry;
header += " main\n";
}
header += macro;
// Emitted last, after every #extension directive above: GLSL requires those to precede any
// non-preprocessor token, and these are real function definitions. The bodies come straight
// from EmuCoreX so the .glsl call sites stay identical between the two trees.
header += R"(
bool gpu_boolean_not(bool value)
{
#if DRIVER_REWRITE_BOOLEAN_NEGATION
return value == false;
#else
return !value;
#endif
}
uvec2 gpu_bitwise_and(uvec2 a, uvec2 b)
{
#if DRIVER_SCALARIZE_VECTOR_BITWISE_AND
return uvec2(a.x & b.x, a.y & b.y);
#else
return a & b;
#endif
}
uvec3 gpu_bitwise_and(uvec3 a, uvec3 b)
{
#if DRIVER_SCALARIZE_VECTOR_BITWISE_AND
return uvec3(a.x & b.x, a.y & b.y, a.z & b.z);
#else
return a & b;
#endif
}
uvec4 gpu_bitwise_and(uvec4 a, uvec4 b)
{
#if DRIVER_SCALARIZE_VECTOR_BITWISE_AND
return uvec4(a.x & b.x, a.y & b.y, a.z & b.z, a.w & b.w);
#else
return a & b;
#endif
}
ivec3 gpu_bitwise_and(ivec3 a, ivec3 b)
{
#if DRIVER_SCALARIZE_VECTOR_BITWISE_AND
return ivec3(a.x & b.x, a.y & b.y, a.z & b.z);
#else
return a & b;
#endif
}
uvec4 gpu_bitwise_not(uvec4 value)
{
#if DRIVER_STORE_BITWISE_NEGATION_IN_TEMPORARY
uvec4 result = ~value;
return result;
#else
return ~value;
#endif
}
)";
return header;
}
std::string GSDeviceOGL::GetVSSource(VSSelector sel)
{
DevCon.WriteLn("GL: Compiling new vertex shader with selector 0x%" PRIX64, sel.key);
std::string macro = fmt::format("#define VS_FST {}\n", static_cast<u32>(sel.fst))
+ fmt::format("#define VS_IIP {}\n", static_cast<u32>(sel.iip))
+ fmt::format("#define VS_POINT_SIZE {}\n", static_cast<u32>(sel.point_size))
+ fmt::format("#define VS_EXPAND {}\n", static_cast<int>(sel.expand));
std::string src = GenGlslHeader("vs_main", GL_VERTEX_SHADER, macro);
src += m_shader_tfx_vgs;
return src;
}
std::string GSDeviceOGL::GetPSSource(const PSSelector& sel)
{
DevCon.WriteLn("GL: Compiling new pixel shader with selector 0x%016" PRIX64 "_%016" PRIX64, sel.key_hi, sel.key_lo);
std::string macro = fmt::format("#define PS_FST {}\n", sel.fst)
+ fmt::format("#define PS_WMS {}\n", sel.wms)
+ fmt::format("#define PS_WMT {}\n", sel.wmt)
+ fmt::format("#define PS_ADJS {}\n", sel.adjs)
+ fmt::format("#define PS_ADJT {}\n", sel.adjt)
+ fmt::format("#define PS_AEM_FMT {}\n", sel.aem_fmt)
+ fmt::format("#define PS_PAL_FMT {}\n", sel.pal_fmt)
+ fmt::format("#define PS_DST_FMT {}\n", sel.dst_fmt)
+ fmt::format("#define PS_DEPTH_FMT {}\n", sel.depth_fmt)
+ fmt::format("#define PS_CHANNEL_FETCH {}\n", sel.channel)
+ fmt::format("#define PS_URBAN_CHAOS_HLE {}\n", sel.urban_chaos_hle)
+ fmt::format("#define PS_TALES_OF_ABYSS_HLE {}\n", sel.tales_of_abyss_hle)
+ fmt::format("#define PS_TEX_IS_FB {}\n", sel.tex_is_fb)
+ fmt::format("#define PS_AEM {}\n", sel.aem)
+ fmt::format("#define PS_TFX {}\n", sel.tfx)
+ fmt::format("#define PS_TCC {}\n", sel.tcc)
+ fmt::format("#define PS_ATST {}\n", static_cast<u32>(sel.atst))
+ fmt::format("#define PS_AFAIL {}\n", static_cast<u32>(sel.afail))
+ fmt::format("#define PS_FOG {}\n", sel.fog)
+ fmt::format("#define PS_BLEND_HW {}\n", sel.blend_hw)
+ fmt::format("#define PS_A_MASKED {}\n", sel.a_masked)
+ fmt::format("#define PS_FBA {}\n", sel.fba)
+ fmt::format("#define PS_LTF {}\n", sel.ltf)
+ fmt::format("#define PS_AUTOMATIC_LOD {}\n", sel.automatic_lod)
+ fmt::format("#define PS_MANUAL_LOD {}\n", sel.manual_lod)
+ fmt::format("#define PS_COLCLIP {}\n", sel.colclip)
+ fmt::format("#define PS_DATE {}\n", sel.date)
+ fmt::format("#define PS_TCOFFSETHACK {}\n", sel.tcoffsethack)
+ fmt::format("#define PS_REGION_RECT {}\n", sel.region_rect)
+ fmt::format("#define PS_BLEND_A {}\n", sel.blend_a)
+ fmt::format("#define PS_BLEND_B {}\n", sel.blend_b)
+ fmt::format("#define PS_BLEND_C {}\n", sel.blend_c)
+ fmt::format("#define PS_BLEND_D {}\n", sel.blend_d)
+ fmt::format("#define PS_IIP {}\n", sel.iip)
+ fmt::format("#define PS_SHUFFLE {}\n", sel.shuffle)
+ fmt::format("#define PS_SHUFFLE_SAME {}\n", sel.shuffle_same)
+ fmt::format("#define PS_PROCESS_BA {}\n", sel.process_ba)
+ fmt::format("#define PS_PROCESS_RG {}\n", sel.process_rg)
+ fmt::format("#define PS_SHUFFLE_ACROSS {}\n", sel.shuffle_across)
+ fmt::format("#define PS_READ16_SRC {}\n", sel.real16src)
+ fmt::format("#define PS_WRITE_RG {}\n", sel.write_rg)
+ fmt::format("#define PS_FBMASK {}\n", sel.fbmask)
+ fmt::format("#define PS_COLCLIP_HW {}\n", sel.colclip_hw)
+ fmt::format("#define PS_RTA_CORRECTION {}\n", sel.rta_correction)
+ fmt::format("#define PS_RTA_SRC_CORRECTION {}\n", sel.rta_source_correction)
+ fmt::format("#define PS_DITHER {}\n", sel.dither)
+ fmt::format("#define PS_DITHER_ADJUST {}\n", sel.dither_adjust)
+ fmt::format("#define PS_ZCLAMP {}\n", sel.zclamp)
+ fmt::format("#define PS_ZFLOOR {}\n", sel.zfloor)
+ fmt::format("#define PS_BLEND_MIX {}\n", sel.blend_mix)
+ fmt::format("#define PS_ROUND_INV {}\n", sel.round_inv)
+ fmt::format("#define PS_FIXED_ONE_A {}\n", sel.fixed_one_a)
+ fmt::format("#define PS_PABE {}\n", sel.pabe)
+ fmt::format("#define PS_SCANMSK {}\n", sel.scanmsk)
+ fmt::format("#define PS_NO_COLOR {}\n", sel.no_color)
+ fmt::format("#define PS_NO_COLOR1 {}\n", sel.no_color1)
+ fmt::format("#define PS_BLEND_FACTOR_IN_ALPHA {}\n", sel.blend_factor_in_alpha)
+ fmt::format("#define PS_ZTST {}\n", sel.ztst)
+ fmt::format("#define PS_AA1 {}\n", static_cast<u32>(sel.aa1))
+ fmt::format("#define PS_ABE {}\n", sel.abe)
+ fmt::format("#define PS_ANISOTROPIC_FILTERING {}\n", sel.sw_aniso)
+ fmt::format("#define PS_ROV_COLOR {}\n", 0)
+ fmt::format("#define PS_ROV_DEPTH {}\n", 0)
;
std::string src = GenGlslHeader("ps_main", GL_FRAGMENT_SHADER, macro);
src += m_shader_tfx_fs;
return src;
}
// Copy a sub part of texture (same as below but force a conversion)
void GSDeviceOGL::BlitRect(GSTexture* sTex, const GSVector4i& r, const GSVector2i& dsize, bool at_origin, Filter filter)
{
g_perfmon.Put(GSPerfMon::TextureCopies, 1);
CommitClear(sTex, true);
GL_PUSH(fmt::format("CopyRectConv from {}", static_cast<GSTextureOGL*>(sTex)->GetID()).c_str());
// NOTE: This previously used glCopyTextureSubImage2D(), but this appears to leak memory in
// the loading screens of Evolution Snowboarding in Intel/NVIDIA drivers.
glDisable(GL_SCISSOR_TEST);
const GSVector4 float_r(r);
GetConvertProgram(ShaderConvert::COPY).Bind();
OMSetDepthStencilState(m_convert.dss);
OMSetBlendState();
OMSetColorMaskState();
PSSetShaderResource(TEXTURE_TEXTURE, sTex);
PSSetSamplerState(filter == Biln ? m_convert.ln : m_convert.pt);
DrawStretchRect(float_r / (GSVector4(sTex->GetSize()).xyxy()), float_r, dsize);
glEnable(GL_SCISSOR_TEST);
}
// Copy a sub part of a texture into another
void GSDeviceOGL::DoCopyRect(GSTexture* sTex, GSTexture* dTex, const GSVector4i& r, u32 destX, u32 destY)
{
// Empty rect, abort copy.
if (r.rempty())
{
GL_INS("GL: DoCopyRect rect empty.");
return;
}
const GLuint& sid = static_cast<GSTextureOGL*>(sTex)->GetID();
const GLuint& did = static_cast<GSTextureOGL*>(dTex)->GetID();
const GSVector4i dst_rect(0, 0, dTex->GetWidth(), dTex->GetHeight());
const bool full_draw_copy = dst_rect.eq(r);
// Source is cleared, if destination is a render target, we can carry the clear forward.
if (sTex->GetState() == GSTexture::State::Cleared)
{
if (dTex->IsRenderTargetOrDepthStencil() && ProcessClearsBeforeCopy(sTex, dTex, full_draw_copy))
return;
// Commit clear for the source texture.
CommitClear(sTex, false);
}
g_perfmon.Put(GSPerfMon::TextureCopies, 1);
GL_PUSH("DoCopyRect from %d to %d", sid, did);
// Commit destination clear if partially overwritten (color only).
if (dTex->GetState() == GSTexture::State::Cleared && !full_draw_copy)
CommitClear(dTex, false);
if (GLAD_GL_VERSION_4_3 || GLAD_GL_ARB_copy_image)
{
glCopyImageSubData(sid, GL_TEXTURE_2D, 0, r.x, r.y, 0, did, GL_TEXTURE_2D,
0, destX, destY, 0, r.width(), r.height(), 1);
}
else if (GLAD_GL_EXT_copy_image)
{
glCopyImageSubDataEXT(sid, GL_TEXTURE_2D, 0, r.x, r.y, 0, did, GL_TEXTURE_2D,
0, destX, destY, 0, r.width(), r.height(), 1);
}
else if (GLAD_GL_NV_copy_image)
{
glCopyImageSubDataNV(sid, GL_TEXTURE_2D, 0, r.x, r.y, 0, did, GL_TEXTURE_2D,
0, destX, destY, 0, r.width(), r.height(), 1);
}
else
{
const bool draw_in_depth = sTex->IsDepthStencil();
const GLenum attachment = draw_in_depth ? GL_DEPTH_STENCIL_ATTACHMENT : GL_COLOR_ATTACHMENT0;
// Bind attachments.
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_fbo_read);
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, attachment, GL_TEXTURE_2D, sid, 0);
glReadBuffer(draw_in_depth ? GL_NONE : GL_COLOR_ATTACHMENT0);
// Do copy.
glCopyTextureSubImage2D(did, 0, destX, destY, r.x, r.y, r.width(), r.height());
// Unbind attachments.
if (draw_in_depth)
glReadBuffer(GL_COLOR_ATTACHMENT0);
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, attachment, GL_TEXTURE_2D, 0, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
}
dTex->SetState(GSTexture::State::Dirty);
}
void GSDeviceOGL::DoStretchRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect,
ShaderConvertSelector shader, Filter filter)
{
const u8 mask = shader.Mask();
shader = shader.SetMask(); // Mask is handled separately from program.
filter = shader.SupportsBilinear() ? Nearest : filter; // Don't allow HW bilinear if SW bilinear is needed.
DoStretchRect(sTex, sRect, dTex, dRect, GetConvertProgram(shader), false, OMColorMaskSelector(mask), filter);
}
void GSDeviceOGL::DoStretchRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect,
const GLProgram& ps, Filter filter)
{
DoStretchRect(sTex, sRect, dTex, dRect, ps, false, OMColorMaskSelector(), filter);
}
void GSDeviceOGL::DoStretchRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect,
const GLProgram& ps, bool alpha_blend, OMColorMaskSelector cms, Filter filter)
{
CommitClear(sTex, true);
const bool draw_in_depth = dTex->IsDepthStencil();
// ************************************
// Init
// ************************************
GL_PUSH("StretchRect from %d to %d", static_cast<GSTextureOGL*>(sTex)->GetID(), static_cast<GSTextureOGL*>(dTex)->GetID());
if (draw_in_depth)
OMSetRenderTargets(nullptr, nullptr, dTex);
else
OMSetRenderTargets(dTex, nullptr, nullptr);
ps.Bind();
// ************************************
// om
// ************************************
if (draw_in_depth)
OMSetDepthStencilState(m_convert.dss_write);
else
OMSetDepthStencilState(m_convert.dss);
OMSetBlendState(alpha_blend, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_FUNC_ADD);
OMSetColorMaskState(cms);
// ************************************
// Texture
// ************************************
PSSetShaderResource(TEXTURE_TEXTURE, sTex);
PSSetSamplerState(filter == Biln ? m_convert.ln : m_convert.pt);
// ************************************
// Draw
// ************************************
DrawStretchRect(sRect, dRect, dTex->GetSize());
}
void GSDeviceOGL::PresentRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect, PresentShader shader, float shaderTime, Filter filter)
{
CommitClear(sTex, true);
const GSVector2i ds(dTex ? dTex->GetSize() : GSVector2i(GetWindowWidth(), GetWindowHeight()));
DisplayConstantBuffer cb;
cb.SetSource(sRect, sTex->GetSize());
cb.SetTarget(dRect, ds);
cb.SetTime(shaderTime);
GLProgram& prog = m_present[static_cast<int>(shader)];
prog.Bind();
prog.Uniform4fv(0, cb.SourceRect.F32);
prog.Uniform4fv(1, cb.TargetRect.F32);
prog.Uniform2fv(2, &cb.SourceSize.x);
prog.Uniform2fv(3, &cb.TargetSize.x);
prog.Uniform2fv(4, &cb.TargetResolution.x);
prog.Uniform2fv(5, &cb.RcpTargetResolution.x);
prog.Uniform2fv(6, &cb.SourceResolution.x);
prog.Uniform2fv(7, &cb.RcpSourceResolution.x);
prog.Uniform1f(8, cb.TimeAndPad.x);
OMSetDepthStencilState(m_convert.dss);
OMSetBlendState(false);
OMSetColorMaskState();
PSSetShaderResource(TEXTURE_TEXTURE, sTex);
PSSetSamplerState(filter == Biln ? m_convert.ln : m_convert.pt);
// Flip y axis only when we render in the backbuffer
// By default everything is render in the wrong order (ie dx).
// 1/ consistency between several pass rendering (interlace)
// 2/ in case some GS code expect thing in dx order.
// Only flipping the backbuffer is transparent (I hope)...
const GSVector4 flip_sr(sRect.xwzy());
DrawStretchRect(flip_sr, dRect, ds);
}
void GSDeviceOGL::DoUpdateCLUTTexture(GSTexture* sTex, float sScale, u32 offsetX, u32 offsetY, GSTexture* dTex, u32 dOffset, u32 dSize)
{
CommitClear(sTex, false);
const ShaderConvert shader = (dSize == 16) ? ShaderConvert::CLUT_4 : ShaderConvert::CLUT_8;
GLProgram& prog = GetConvertProgram(shader);
prog.Bind();
prog.Uniform3ui(0, offsetX, offsetY, dOffset);
prog.Uniform1f(1, sScale);
OMSetDepthStencilState(m_convert.dss);
OMSetBlendState(false);
OMSetColorMaskState();
OMSetRenderTargets(dTex, nullptr, nullptr);
PSSetShaderResource(TEXTURE_TEXTURE, sTex);
PSSetSamplerState(m_convert.pt);
const GSVector4 dRect(0, 0, dSize, 1);
DrawStretchRect(GSVector4::zero(), dRect, dTex->GetSize());
}
void GSDeviceOGL::DoConvertToIndexedTexture(GSTexture* sTex, float sScale, u32 offsetX, u32 offsetY, u32 SBW, u32 SPSM, GSTexture* dTex, u32 DBW, u32 DPSM)
{
CommitClear(sTex, false);
const ShaderConvert shader = ((SPSM & 0xE) == 0) ? ShaderConvert::RGBA_TO_8I : ShaderConvert::RGB5A1_TO_8I;
GLProgram& prog = GetConvertProgram(shader);
prog.Bind();
prog.Uniform1ui(0, SBW);
prog.Uniform1ui(1, DBW);
prog.Uniform1ui(2, SPSM);
prog.Uniform1f(3, sScale);
OMSetDepthStencilState(m_convert.dss);
OMSetBlendState(false);
OMSetColorMaskState();
OMSetRenderTargets(dTex, nullptr, nullptr);
PSSetShaderResource(TEXTURE_TEXTURE, sTex);
PSSetSamplerState(m_convert.pt);
const GSVector4 dRect(0, 0, dTex->GetWidth(), dTex->GetHeight());
DrawStretchRect(GSVector4::zero(), dRect, dTex->GetSize());
}
void GSDeviceOGL::DoFilteredDownsampleTexture(GSTexture* sTex, GSTexture* dTex, u32 downsample_factor, const GSVector2i& clamp_min, const GSVector4& dRect)
{
CommitClear(sTex, false);
constexpr ShaderConvert shader = ShaderConvert::DOWNSAMPLE_COPY;
GLProgram& prog = GetConvertProgram(shader);
prog.Bind();
prog.Uniform2iv(0, clamp_min.v);
prog.Uniform1i(1, downsample_factor);
prog.Uniform1f(2, static_cast<float>(downsample_factor * downsample_factor));
prog.Uniform1f(3, (GSConfig.UserHacks_NativeScaling > GSNativeScaling::Aggressive) ? 2.0f : 1.0f);
OMSetDepthStencilState(m_convert.dss);
OMSetBlendState(false);
OMSetColorMaskState();
OMSetRenderTargets(dTex, nullptr, nullptr);
PSSetShaderResource(TEXTURE_TEXTURE, sTex);
PSSetSamplerState(m_convert.pt);
//const GSVector4 dRect = GSVector4(dTex->GetRect());
DrawStretchRect(GSVector4::zero(), dRect, dTex->GetSize());
}
void GSDeviceOGL::DrawStretchRect(const GSVector4& sRect, const GSVector4& dRect, const GSVector2i& ds)
{
g_perfmon.Put(GSPerfMon::TextureCopies, 1);
const float inv_x = 2.0f / ds.x;
const float inv_y = 2.0f / ds.y;
const float left = dRect.x * inv_x - 1.0f;
const float right = dRect.z * inv_x - 1.0f;
const float top = -1.0f + dRect.y * inv_y;
const float bottom = -1.0f + dRect.w * inv_y;
const GSVertexPT1 vertices[] =
{
{GSVector4(left , top , 0.0f, 0.0f) , GSVector2(sRect.x , sRect.y)} ,
{GSVector4(right , top , 0.0f, 0.0f) , GSVector2(sRect.z , sRect.y)} ,
{GSVector4(left , bottom, 0.0f, 0.0f) , GSVector2(sRect.x , sRect.w)} ,
{GSVector4(right , bottom, 0.0f, 0.0f) , GSVector2(sRect.z , sRect.w)} ,
};
IASetVAO(m_vao);
IASetVertexBuffer(vertices, 4);
IASetPrimitiveTopology(GL_TRIANGLE_STRIP);
DrawPrimitive();
}
void GSDeviceOGL::DoDrawMultiStretchRects(
const MultiStretchRect* rects, u32 num_rects, GSTexture* dTex, ShaderConvertSelector shader)
{
shader = shader.SetMask(); // Mask is handled separately from program.
IASetVAO(m_vao);
IASetPrimitiveTopology(GL_TRIANGLE_STRIP);
OMSetDepthStencilState(shader.DepthOutput() ? m_convert.dss_write : m_convert.dss);
OMSetBlendState(false);
OMSetColorMaskState();
if (!dTex->IsDepthStencil())
OMSetRenderTargets(dTex, nullptr, nullptr);
else
OMSetRenderTargets(nullptr, nullptr, dTex);
GetConvertProgram(shader).Bind();
const GSVector2 ds(static_cast<float>(dTex->GetWidth()), static_cast<float>(dTex->GetHeight()));
GSTexture* last_tex = rects[0].src;
Filter last_filter = rects[0].filter;
u8 last_wmask = rects[0].wmask.wrgba;
u32 first = 0;
u32 count = 1;
for (u32 i = 1; i < num_rects; i++)
{
if (rects[i].src == last_tex && rects[i].filter == last_filter && rects[i].wmask.wrgba == last_wmask)
{
count++;
continue;
}
DoMultiStretchRects(rects + first, count, ds);
last_tex = rects[i].src;
last_filter = rects[i].filter;
last_wmask = rects[i].wmask.wrgba;
first += count;
count = 1;
}
DoMultiStretchRects(rects + first, count, ds);
}
void GSDeviceOGL::DoMultiStretchRects(const MultiStretchRect* rects, u32 num_rects, const GSVector2& ds)
{
g_perfmon.Put(GSPerfMon::TextureCopies, 1);
const u32 vertex_reserve_size = num_rects * 4 * sizeof(GSVertexPT1);
const u32 index_reserve_size = num_rects * 6 * sizeof(u16);
auto vertex_map = m_vertex_stream_buffer->Map(sizeof(GSVertexPT1), vertex_reserve_size);
auto index_map = m_index_stream_buffer->Map(sizeof(u16), index_reserve_size);
m_vertex.start = vertex_map.index_aligned;
m_index.start = index_map.index_aligned;
// Don't use primitive restart here, it ends up slower on some drivers.
GSVertexPT1* verts = reinterpret_cast<GSVertexPT1*>(vertex_map.pointer);
u16* idx = reinterpret_cast<u16*>(index_map.pointer);
u32 icount = 0;
u32 vcount = 0;
for (u32 i = 0; i < num_rects; i++)
{
const GSVector4& sRect = rects[i].src_rect;
const GSVector4& dRect = rects[i].dst_rect;
const float inv_x = 2.0f / ds.x;
const float inv_y = 2.0f / ds.y;
const float left = dRect.x * inv_x - 1.0f;
const float right = dRect.z * inv_x - 1.0f;
const float top = -1.0f + dRect.y * inv_y;
const float bottom = -1.0f + dRect.w * inv_y;
const u32 vstart = vcount;
verts[vcount++] = { GSVector4(left , top , 0.0f, 0.0f) , GSVector2(sRect.x , sRect.y) };
verts[vcount++] = { GSVector4(right , top , 0.0f, 0.0f) , GSVector2(sRect.z , sRect.y) };
verts[vcount++] = { GSVector4(left , bottom, 0.0f, 0.0f) , GSVector2(sRect.x , sRect.w) };
verts[vcount++] = { GSVector4(right , bottom, 0.0f, 0.0f) , GSVector2(sRect.z , sRect.w) };
if (i > 0)
idx[icount++] = vstart;
idx[icount++] = vstart;
idx[icount++] = vstart + 1;
idx[icount++] = vstart + 2;
idx[icount++] = vstart + 3;
idx[icount++] = vstart + 3;
};
m_vertex.count = vcount;
m_index.count = icount;
m_vertex_stream_buffer->Unmap(vcount * sizeof(GSVertexPT1));
m_index_stream_buffer->Unmap(icount * sizeof(u16));
PSSetShaderResource(TEXTURE_TEXTURE, rects[0].src);
PSSetSamplerState(rects[0].filter == Biln ? m_convert.ln : m_convert.pt);
OMSetColorMaskState(rects[0].wmask);
DrawIndexedPrimitive();
}
void GSDeviceOGL::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, GSVector4* dRect, const GSRegPMODE& PMODE, const GSRegEXTBUF& EXTBUF, u32 c, const Filter filter)
{
GL_PUSH("DoMerge");
const GSVector4 full_r(0.0f, 0.0f, 1.0f, 1.0f);
const bool feedback_write_2 = PMODE.EN2 && sTex[2] != nullptr && EXTBUF.FBIN == 1;
const bool feedback_write_1 = PMODE.EN1 && sTex[2] != nullptr && EXTBUF.FBIN == 0;
const bool feedback_write_2_but_blend_bg = feedback_write_2 && PMODE.SLBG == 1;
// Merge the 2 source textures (sTex[0],sTex[1]). Final results go to dTex. Feedback write will go to sTex[2].
// If either 2nd output is disabled or SLBG is 1, a background color will be used.
// Note: background color is also used when outside of the unit rectangle area
OMSetColorMaskState();
ClearRenderTarget(dTex, c);
if (sTex[1] && (PMODE.SLBG == 0 || feedback_write_2_but_blend_bg))
{
// 2nd output is enabled and selected. Copy it to destination so we can blend it with 1st output
// Note: value outside of dRect must contains the background color (c)
StretchRect(sTex[1], sRect[1], dTex, PMODE.SLBG ? dRect[2] : dRect[1], ShaderConvert::COPY, filter);
}
// Upload constant to select YUV algo
if (feedback_write_2 || feedback_write_1)
{
// Write result to feedback loop
GetConvertProgram(ShaderConvert::YUV).Bind();
GetConvertProgram(ShaderConvert::YUV).Uniform2i(0, EXTBUF.EMODA, EXTBUF.EMODC);
}
// Save 2nd output
if (feedback_write_2)
StretchRect(dTex, full_r, sTex[2], dRect[2], ShaderConvert::YUV, filter);
// Restore background color to process the normal merge
if (feedback_write_2_but_blend_bg)
ClearRenderTarget(dTex, c);
if (sTex[0])
{
if (PMODE.AMOD == 1) // Keep the alpha from the 2nd output
OMSetColorMaskState(OMColorMaskSelector(0x7));
// 1st output is enabled. It must be blended
if (PMODE.MMOD == 1)
{
// Blend with a constant alpha
m_merge_obj.ps[1].Bind();
m_merge_obj.ps[1].Uniform4fv(0, GSVector4::unorm8(c).v);
DoStretchRect(sTex[0], sRect[0], dTex, dRect[0], m_merge_obj.ps[1], true, OMColorMaskSelector(), filter);
}
else
{
// Blend with 2 * input alpha
DoStretchRect(sTex[0], sRect[0], dTex, dRect[0], m_merge_obj.ps[0], true, OMColorMaskSelector(), filter);
}
}
if (feedback_write_1)
StretchRect(dTex, full_r, sTex[2], dRect[2], ShaderConvert::YUV, filter);
}
void GSDeviceOGL::DoInterlace(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect, ShaderInterlace shader, Filter filter, const InterlaceConstantBuffer& cb)
{
OMSetColorMaskState();
m_interlace.ps[static_cast<int>(shader)].Bind();
m_interlace.ps[static_cast<int>(shader)].Uniform4fv(0, cb.ZrH.F32);
DoStretchRect(sTex, sRect, dTex, dRect, m_interlace.ps[static_cast<int>(shader)], filter);
}
bool GSDeviceOGL::CompileFXAAProgram()
{
const std::string_view fxaa_macro = "#define FXAA_GLSL_130 1\n";
const std::optional<std::string> shader = ReadShaderSource("shaders/common/fxaa.fx");
if (!shader.has_value())
{
Console.Error("GL: Failed to read fxaa.fs");
return false;
}
const std::string ps(GetShaderSource("main", GL_FRAGMENT_SHADER, shader->c_str(), fxaa_macro));
std::optional<GLProgram> prog = m_shader_cache.GetProgram(m_convert.vs, ps);
if (!prog.has_value())
{
Console.Error("GL: Failed to compile FXAA fragment shader");
return false;
}
m_fxaa.ps = std::move(prog.value());
return true;
}
void GSDeviceOGL::DoFXAA(GSTexture* sTex, GSTexture* dTex)
{
if (!m_fxaa.ps.IsValid())
return;
GL_PUSH("DoFxaa");
OMSetColorMaskState();
const GSVector2i s = dTex->GetSize();
const GSVector4 sRect(0, 0, 1, 1);
const GSVector4 dRect(0, 0, s.x, s.y);
DoStretchRect(sTex, sRect, dTex, dRect, m_fxaa.ps, Biln);
}
#ifdef ARMSX2_HAS_LIBRASHADER
static void ReportShaderChainError(const char* what, libra_error_t err)
{
char* msg = nullptr;
if (libra_error_write(err, &msg) == 0 && msg)
{
Console.Error("(GS) librashader GL: %s failed: %s", what, msg);
libra_error_free_string(&msg);
}
else
{
Console.Error("(GS) librashader GL: %s failed (errno %d)", what, static_cast<int>(libra_error_errno(err)));
}
libra_error_free(&err);
}
// libra_gl_loader_t is a bare C function pointer with no userdata argument, so the context
// has to be reachable without one. Only ever one GL device exists at a time (g_gs_device),
// and the chain is created and run on the GS thread, so a file-static set at create time is
// enough — no need to thread the device through.
static GLContext* s_shader_chain_gl_context = nullptr;
static const void* ShaderChainGLLoader(const char* name)
{
return s_shader_chain_gl_context ? s_shader_chain_gl_context->GetProcAddress(name) : nullptr;
}
#endif
void GSDeviceOGL::DestroyShaderChain()
{
#ifdef ARMSX2_HAS_LIBRASHADER
if (m_shader_chain)
{
libra_gl_filter_chain_t chain = static_cast<libra_gl_filter_chain_t>(m_shader_chain);
libra_gl_filter_chain_free(&chain);
m_shader_chain = nullptr;
}
#endif
m_shader_chain_preset.clear();
m_shader_chain_failed = false;
m_shader_frame_count = 0;
m_shader_param_generation = 0;
}
void GSDeviceOGL::ApplyShaderChainParams()
{
#ifdef ARMSX2_HAS_LIBRASHADER
// Per-frame fast path: one atomic load. The lock and the copy only happen on the
// frames where the user actually moved something.
const u64 generation = GetShaderChainParamGeneration();
if (generation == m_shader_param_generation)
return;
std::vector<std::pair<std::string, float>> params;
if (GetShaderChainParams(m_shader_chain_preset, &params))
{
libra_gl_filter_chain_t chain = static_cast<libra_gl_filter_chain_t>(m_shader_chain);
for (const auto& [name, value] : params)
{
// A preset can be swapped under a stale override set, so an unknown parameter
// name is a routine miss, not a fault: report nothing and keep going, since
// the remaining names are still valid.
if (libra_error_t err = libra_gl_filter_chain_set_param(&chain, name.c_str(), value))
libra_error_free(&err);
}
}
// Set even when the store held another preset's values or none at all — otherwise this
// re-runs the lookup on every frame for as long as the generation stays ahead.
m_shader_param_generation = generation;
#endif
}
void GSDeviceOGL::RestoreGLStateAfterShaderChain()
{
// librashader backs up and restores the fixed-function *enables* itself (BLEND,
// CULL_FACE, DEPTH_TEST, STENCIL_TEST, SCISSOR_TEST, and PRIMITIVE_RESTART /
// FRAMEBUFFER_SRGB where supported) via an RAII guard, and it never touches the blend
// func/equation, depth func/mask, stencil func/op, or the scissor box — so all of those
// stay in sync with GLState on their own. What it does clobber and leave behind is the
// bound FBO (documented as "GL_FRAMEBUFFER is bound to 0" on return), VAO, program,
// texture units, sampler bindings, the viewport and the colour mask.
//
// GLState's setters are all early-return-if-unchanged, so a stale cache means the state
// would never be re-pushed and the next draw would silently render wrong. Rather than
// invalidate the cache (it has no "unknown" sentinel — GLState::Clear() asserts defaults,
// which is only true on a fresh context), push the cached values back into the driver so
// reality matches the cache again. Same shape as the end of RenderImGui().
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLState::fbo);
glBindVertexArray(GLState::vao);
glViewport(0, 0, GLState::viewport.x, GLState::viewport.y);
const OMColorMaskSelector cms(GLState::wrgba);
glColorMaski(0, cms.wr, cms.wg, cms.wb, cms.wa);
// The chain binds a texture and a sampler per pass, starting at unit 0. Only the units
// GLState tracks matter; anything above them is never sampled by the TFX/utility draws.
for (u32 i = 0; i < std::size(GLState::tex_unit); i++)
glBindTextureUnit(i, GLState::tex_unit[i]);
// Sampler unit 0 is the tracked one. Unit 1 is special: m_palette_ss is bound there once
// at device creation and is expected to stay put for the device's lifetime (a sampler
// object can't be shared across image units, hence the dedicated one), so it has to be
// put back explicitly or every paletted texture silently samples wrong from here on.
// GSDeviceOGL never binds a sampler to the remaining units and expects them to fall back
// to the texture's own parameters, so any the chain left behind have to come back off.
glBindSampler(0, GLState::ps_ss);
glBindSampler(1, m_palette_ss);
for (u32 i = 2; i < std::size(GLState::tex_unit); i++)
glBindSampler(i, 0);
// The program cache is a file-static in GLProgram rather than part of GLState, and it
// has a purpose-built invalidator; the next Bind() re-pushes.
GLProgram::ResetLastProgram();
}
bool GSDeviceOGL::DoApplyShaderChain(GSTexture* sTex, GSTexture* dTex)
{
#ifndef ARMSX2_HAS_LIBRASHADER
return false;
#else
// A preset that fails to compile must not be retried every frame — that would run a
// full slang compile 60x/sec. Latch the failure until the user picks another preset.
if (m_shader_chain_failed && m_shader_chain_preset == GSConfig.ShaderChainPreset)
return false;
if (!m_shader_chain || m_shader_chain_preset != GSConfig.ShaderChainPreset)
{
DestroyShaderChain();
m_shader_chain_preset = GSConfig.ShaderChainPreset;
libra_shader_preset_t preset = nullptr;
if (libra_error_t err = libra_preset_create(m_shader_chain_preset.c_str(), &preset))
{
ReportShaderChainError("preset load", err);
m_shader_chain_failed = true;
return false;
}
filter_chain_gl_opt_t opt = {};
opt.version = LIBRASHADER_CURRENT_VERSION;
// 0 means "detect from the context": librashader asks glow for the version and picks
// GLSL 300/310/320 ES on an ES context. Hardcoding 330 would emit desktop GLSL and
// break every GLES device.
opt.glsl_version = 0;
// DSA is GL 4.5+ and simply does not exist on GLES. This implicitly disables
// librashader's own shader cache, which is the documented trade-off.
opt.use_dsa = false;
opt.force_no_mipmaps = false;
opt.disable_cache = false;
// librashader resolves GL through this loader rather than linking it, the same way
// the Vulkan chain rides the user's ICD.
s_shader_chain_gl_context = m_gl_context.get();
// create() invalidates `preset` unconditionally ("the shader preset is
// immediately invalidated"), so it must NOT be freed afterwards on either path.
libra_gl_filter_chain_t chain = nullptr;
if (libra_error_t err = libra_gl_filter_chain_create(&preset, ShaderChainGLLoader, &opt, &chain))
{
ReportShaderChainError("chain create", err);
m_shader_chain_failed = true;
return false;
}
m_shader_chain = chain;
m_shader_frame_count = 0;
// The new chain sits at the preset's initial values, so whatever we last pushed is
// gone with the old one — force ApplyShaderChainParams to feed it again.
m_shader_param_generation = 0;
Console.WriteLn("(GS) librashader GL: loaded preset '%s'", m_shader_chain_preset.c_str());
}
// GS thread, chain alive, before the frame call — the only place a set_param is safe.
// Touches no GL state of its own (it writes librashader's own parameter map), so it is
// fine ahead of the GL_PUSH and needs no state restore.
ApplyShaderChainParams();
GL_PUSH("ApplyShaderChain");
GSTextureOGL* const src = static_cast<GSTextureOGL*>(sTex);
GSTextureOGL* const dst = static_cast<GSTextureOGL*>(dTex);
// librashader wants the sized internal format the texture was allocated with — that's
// m_gl_format (GL_RGBA8), the one handed to glTextureStorage2D, NOT m_int_format, which
// despite the name is the pixel-transfer format (GL_RGBA).
const libra_image_gl_t in = {src->GetID(), src->GetGLFormat(),
static_cast<uint32_t>(src->GetWidth()), static_cast<uint32_t>(src->GetHeight())};
const libra_image_gl_t out = {dst->GetID(), dst->GetGLFormat(),
static_cast<uint32_t>(dst->GetWidth()), static_cast<uint32_t>(dst->GetHeight())};
const libra_viewport_t vp = {0.0f, 0.0f,
static_cast<uint32_t>(dst->GetWidth()), static_cast<uint32_t>(dst->GetHeight())};
// Every librashader entry point takes the chain handle by address, not by value.
// Unlike Vulkan there's no command buffer — the chain issues its draws immediately.
libra_gl_filter_chain_t chain = static_cast<libra_gl_filter_chain_t>(m_shader_chain);
const libra_error_t err = libra_gl_filter_chain_frame(&chain, m_shader_frame_count, in, out, &vp, nullptr, nullptr);
// Unconditional: the chain can fail part-way through, having already clobbered state.
RestoreGLStateAfterShaderChain();
if (err)
{
ReportShaderChainError("frame", err);
m_shader_chain_failed = true;
return false;
}
m_shader_frame_count++;
dst->SetState(GSTexture::State::Dirty);
return true;
#endif
}
bool GSDeviceOGL::CompileShadeBoostProgram()
{
const std::optional<std::string> shader = ReadShaderSource("shaders/opengl/shadeboost.glsl");
if (!shader.has_value())
{
Host::ReportErrorAsync("GS", "Failed to read shaders/opengl/shadeboost.glsl.");
return false;
}
const std::string ps(GetShaderSource("ps_main", GL_FRAGMENT_SHADER, *shader));
if (!m_shader_cache.GetProgram(&m_shadeboost.ps, m_convert.vs, ps))
return false;
m_shadeboost.ps.RegisterUniform("params");
m_shadeboost.ps.SetName("Shadeboost pipe");
return true;
}
void GSDeviceOGL::DoShadeBoost(GSTexture* sTex, GSTexture* dTex, const float params[4])
{
GL_PUSH("DoShadeBoost");
m_shadeboost.ps.Bind();
m_shadeboost.ps.Uniform4fv(0, params);
OMSetColorMaskState();
const GSVector2i s = dTex->GetSize();
const GSVector4 sRect(0, 0, 1, 1);
const GSVector4 dRect(0, 0, s.x, s.y);
DoStretchRect(sTex, sRect, dTex, dRect, m_shadeboost.ps, Nearest);
}
void GSDeviceOGL::SetupDATE(GSTexture* rt, GSTexture* ds, SetDATM datm, const GSVector4i& bbox)
{
g_perfmon.Put(GSPerfMon::TextureCopies, 1);
GL_PUSH("DATE First Pass");
// sfex3 (after the capcom logo), vf4 (first menu fading in), ffxii shadows, rumble roses shadows, persona4 shadows
OMSetRenderTargets(nullptr, nullptr, ds, &GLState::scissor);
{
constexpr GLint clear_color = 0;
glClearBufferiv(GL_STENCIL, 0, &clear_color);
}
GetConvertProgram(SetDATMShader(datm)).Bind();
// om
OMSetDepthStencilState(m_date.dss);
OMSetBlendState(false);
OMSetColorMaskState();
// ia
const GSVector4 src = GSVector4(bbox) / GSVector4(ds->GetSize()).xyxy();
const GSVector4 dst = src * 2.f - 1.f;
const GSVertexPT1 vertices[] =
{
{GSVector4(dst.x, dst.y, 0.0f, 0.0f), GSVector2(src.x, src.y)},
{GSVector4(dst.z, dst.y, 0.0f, 0.0f), GSVector2(src.z, src.y)},
{GSVector4(dst.x, dst.w, 0.0f, 0.0f), GSVector2(src.x, src.w)},
{GSVector4(dst.z, dst.w, 0.0f, 0.0f), GSVector2(src.z, src.w)},
};
IASetVAO(m_vao);
IASetVertexBuffer(vertices, 4);
IASetPrimitiveTopology(GL_TRIANGLE_STRIP);
// Texture
PSSetShaderResource(TEXTURE_TEXTURE, rt);
PSSetSamplerState(m_convert.pt);
DrawPrimitive();
}
__fi static void WriteToStreamBuffer(GLStreamBuffer* sb, u32 index, u32 align, const void* data, u32 size)
{
const auto res = sb->Map(align, size);
std::memcpy(res.pointer, data, size);
sb->Unmap(size);
glBindBufferRange(GL_UNIFORM_BUFFER, index, sb->GetGLBufferId(), res.buffer_offset, size);
}
void GSDeviceOGL::VSSetUniformBuffer(GSHWDrawConfig::VSConstantBuffer& cb)
{
WriteToStreamBuffer(m_vertex_uniform_stream_buffer.get(), g_vs_cb_index,
m_uniform_buffer_alignment, &cb, sizeof(cb));
}
void GSDeviceOGL::PSSetUniformBuffer(GSHWDrawConfig::PSConstantBuffer& cb)
{
WriteToStreamBuffer(m_fragment_uniform_stream_buffer.get(), g_ps_cb_index,
m_uniform_buffer_alignment, &cb, sizeof(cb));
}
void GSDeviceOGL::VSSetPushConstants(u32 base_vertex, u32 base_index, bool force_update)
{
GSHWDrawConfig::VSPushConstants vs_pc;
vs_pc.base_vertex = base_vertex;
vs_pc.base_index = base_index;
if (m_vs_pc_cache.Update(vs_pc) || force_update)
{
WriteToStreamBuffer(m_vertex_push_constants_stream_buffer.get(), g_vs_pc_index,
m_uniform_buffer_alignment, &vs_pc, sizeof(vs_pc));
}
}
void GSDeviceOGL::IASetVAO(GLuint vao)
{
if (GLState::vao == vao)
return;
GLState::vao = vao;
glBindVertexArray(vao);
}
void GSDeviceOGL::IASetVertexBuffer(const void* vertices, size_t count, size_t align_multiplier)
{
const u32 size = static_cast<u32>(count) * sizeof(GSVertexPT1);
auto res = m_vertex_stream_buffer->Map(sizeof(GSVertexPT1) * align_multiplier, size);
std::memcpy(res.pointer, vertices, size);
m_vertex.start = res.index_aligned * align_multiplier;
m_vertex.count = count;
m_vertex_stream_buffer->Unmap(size);
}
void GSDeviceOGL::SetIndexBuffer(std::unique_ptr<GLStreamBuffer>& buffer, const void* index, size_t count)
{
const u32 size = static_cast<u32>(count) * sizeof(u16);
auto res = buffer->Map(sizeof(u16), size);
m_index.start = res.index_aligned;
m_index.count = count;
std::memcpy(res.pointer, index, size);
buffer->Unmap(size);
}
void GSDeviceOGL::IASetIndexBuffer(const void* index, size_t count)
{
SetIndexBuffer(m_index_stream_buffer, index, count);
}
void GSDeviceOGL::VSSetIndexBuffer(const void* index, size_t count)
{
SetIndexBuffer(m_expand_index_stream_buffer, index, count);
}
void GSDeviceOGL::IASetPrimitiveTopology(GLenum topology)
{
m_draw_topology = topology;
}
void GSDeviceOGL::PSSetShaderResource(int i, GSTexture* sr)
{
pxAssert(i < static_cast<int>(std::size(GLState::tex_unit)));
const GLuint id = sr ? static_cast<GSTextureOGL*>(sr)->GetID() : 0;
if (GLState::tex_unit[i] != id)
{
GLState::tex_unit[i] = id;
glBindTextureUnit(i, id);
}
}
void GSDeviceOGL::PSSetSamplerState(GLuint ss)
{
if (GLState::ps_ss != ss)
{
GLState::ps_ss = ss;
glBindSampler(0, ss);
}
}
void GSDeviceOGL::ClearSamplerCache()
{
glDeleteSamplers(std::size(m_ps_ss), m_ps_ss);
for (u32 key = 0; key < std::size(m_ps_ss); key++)
{
m_ps_ss[key] = CreateSampler(PSSamplerSelector(key));
}
}
bool GSDeviceOGL::CreateCASPrograms()
{
std::optional<std::string> cas_source = ReadShaderSource("shaders/opengl/cas.glsl");
if (!cas_source.has_value() || !GetCASShaderSource(&cas_source.value()))
{
m_features.cas_sharpening = false;
return false;
}
const char* header =
"#version 420\n"
"#extension GL_ARB_compute_shader : require\n";
const char* sharpen_params[2] = {
"#define CAS_SHARPEN_ONLY false\n",
"#define CAS_SHARPEN_ONLY true\n"};
if (!m_shader_cache.GetComputeProgram(&m_cas.upscale_ps, fmt::format("{}{}{}", header, sharpen_params[0], cas_source.value())) ||
!m_shader_cache.GetComputeProgram(&m_cas.sharpen_ps, fmt::format("{}{}{}", header, sharpen_params[1], cas_source.value())))
{
m_features.cas_sharpening = false;
return false;
}
const auto link_uniforms = [](GLProgram& prog) {
prog.RegisterUniform("const0");
prog.RegisterUniform("const1");
prog.RegisterUniform("srcOffset");
};
link_uniforms(m_cas.upscale_ps);
link_uniforms(m_cas.sharpen_ps);
return true;
}
bool GSDeviceOGL::DoCAS(GSTexture* sTex, GSTexture* dTex, bool sharpen_only, const std::array<u32, NUM_CAS_CONSTANTS>& constants)
{
g_perfmon.Put(GSPerfMon::TextureCopies, 1);
const GLProgram& prog = sharpen_only ? m_cas.sharpen_ps : m_cas.upscale_ps;
prog.Bind();
prog.Uniform4uiv(0, &constants[0]);
prog.Uniform4uiv(1, &constants[4]);
prog.Uniform2iv(2, reinterpret_cast<const s32*>(&constants[8]));
PSSetShaderResource(TEXTURE_TEXTURE, sTex);
glBindImageTexture(0, static_cast<GSTextureOGL*>(dTex)->GetID(), 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
static const int threadGroupWorkRegionDim = 16;
const int dispatchX = (dTex->GetWidth() + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
const int dispatchY = (dTex->GetHeight() + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
glDispatchCompute(dispatchX, dispatchY, 1);
// dTex is written through an image binding, but the caller turns straight around and
// samples it for the present blit. Image stores are incoherent without an explicit
// barrier, so the fetch is otherwise free to observe the pre-dispatch contents.
glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT);
return true;
}
bool GSDeviceOGL::CreateImGuiProgram()
{
const std::optional<std::string> glsl = ReadShaderSource("shaders/opengl/imgui.glsl");
if (!glsl.has_value())
{
Console.Error("GL: Failed to read imgui.glsl");
return false;
}
std::optional<GLProgram> prog = m_shader_cache.GetProgram(
GetShaderSource("vs_main", GL_VERTEX_SHADER, glsl.value()),
GetShaderSource("ps_main", GL_FRAGMENT_SHADER, glsl.value()));
if (!prog.has_value())
{
Console.Error("GL: Failed to compile imgui shaders");
return false;
}
prog->SetName("ImGui Render");
prog->RegisterUniform("ProjMtx");
m_imgui.ps = std::move(prog.value());
// Need a different VAO because the layout doesn't match GS
glGenVertexArrays(1, &m_imgui.vao);
glBindVertexArray(m_imgui.vao);
m_vertex_stream_buffer->Bind();
m_index_stream_buffer->Bind();
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, pos));
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, uv));
glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, col));
glBindVertexArray(GLState::vao);
return true;
}
void GSDeviceOGL::RenderImGui()
{
ImGui::Render();
const ImDrawData* draw_data = ImGui::GetDrawData();
if (draw_data->CmdListsCount == 0)
return;
UpdateImGuiTextures();
constexpr float L = 0.0f;
const float R = static_cast<float>(m_window_info.surface_width);
constexpr float T = 0.0f;
const float B = static_cast<float>(m_window_info.surface_height);
// clang-format off
const float ortho_projection[4][4] =
{
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
{ 0.0f, 0.0f, -1.0f, 0.0f },
{ (R+L)/(L-R), (T+B)/(B-T), 0.0f, 1.0f },
};
// clang-format on
m_imgui.ps.Bind();
m_imgui.ps.UniformMatrix4fv(0, &ortho_projection[0][0]);
IASetVAO(m_imgui.vao);
OMSetBlendState(true, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_FUNC_ADD);
OMSetDepthStencilState(m_convert.dss);
PSSetSamplerState(m_convert.ln);
// Need to flip the scissor due to lower-left on the window framebuffer
GSVector4i last_scissor = GSVector4i::xffffffff();
// Render command lists
for (int n = 0; n < draw_data->CmdListsCount; n++)
{
const ImDrawList* cmd_list = draw_data->CmdLists[n];
// Different vertex format.
u32 vertex_start;
{
const u32 size = static_cast<u32>(cmd_list->VtxBuffer.Size) * sizeof(ImDrawVert);
auto res = m_vertex_stream_buffer->Map(sizeof(ImDrawVert), size);
std::memcpy(res.pointer, cmd_list->VtxBuffer.Data, size);
vertex_start = res.index_aligned;
m_vertex_stream_buffer->Unmap(size);
}
IASetIndexBuffer(cmd_list->IdxBuffer.Data, cmd_list->IdxBuffer.Size);
for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++)
{
const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i];
pxAssert(!pcmd->UserCallback);
const GSVector4 clip = GSVector4::load<false>(&pcmd->ClipRect);
if ((clip.zwzw() <= clip.xyxy()).mask() != 0)
continue;
// Apply scissor/clipping rectangle (Y is inverted in OpenGL)
const GSVector4i iclip = GSVector4i(clip);
if (!last_scissor.eq(iclip))
{
glScissor(iclip.x, m_window_info.surface_height - iclip.w, iclip.width(), iclip.height());
last_scissor = iclip;
}
// Since we don't have the GSTexture...
const GLuint texture_id = static_cast<GLuint>(pcmd->GetTexID());
if (GLState::tex_unit[0] != texture_id)
{
GLState::tex_unit[0] = texture_id;
glBindTextureUnit(0, texture_id);
}
glDrawElementsBaseVertex(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, GL_UNSIGNED_SHORT,
(void*)(intptr_t)((pcmd->IdxOffset + m_index.start) * sizeof(ImDrawIdx)), pcmd->VtxOffset + vertex_start);
}
g_perfmon.Put(GSPerfMon::DrawCalls, cmd_list->CmdBuffer.Size);
}
IASetVAO(m_vao);
glScissor(GLState::scissor.x, GLState::scissor.y, GLState::scissor.width(), GLState::scissor.height());
}
void GSDeviceOGL::RenderBlankFrame()
{
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDisable(GL_SCISSOR_TEST);
if (m_is_gles) // GLES/TBDR-only tile-bandwidth hint; inert on desktop, gated to keep it canonical
{
const GLenum pre[] = {GL_COLOR, GL_DEPTH, GL_STENCIL};
glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(pre), pre);
}
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if (GLAD_GL_VERSION_4_3 || m_is_gles)
{
const GLenum post[] = {GL_DEPTH, GL_STENCIL};
glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(post), post);
}
m_gl_context->SwapBuffers();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLState::fbo);
glEnable(GL_SCISSOR_TEST);
}
void GSDeviceOGL::OMAttachRt(GSTexture* rt)
{
if (GLState::rt == rt)
return;
GLState::rt = static_cast<GSTextureOGL*>(rt);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
rt ? static_cast<GSTextureOGL*>(rt)->GetID() : 0, 0);
}
void GSDeviceOGL::OMAttachDsAsRt(GSTexture* ds_as_rt)
{
if (GLState::ds_as_rt == ds_as_rt)
return;
GLState::ds_as_rt = static_cast<GSTextureOGL*>(ds_as_rt);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D,
ds_as_rt ? static_cast<GSTextureOGL*>(ds_as_rt)->GetID() : 0, 0);
}
void GSDeviceOGL::OMAttachDs(GSTexture* ds)
{
if (GLState::ds == ds)
return;
GLState::ds = static_cast<GSTextureOGL*>(ds);
const GLenum target = m_features.framebuffer_fetch ? GL_DEPTH_ATTACHMENT : GL_DEPTH_STENCIL_ATTACHMENT;
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, target, GL_TEXTURE_2D, ds ? static_cast<GSTextureOGL*>(ds)->GetID() : 0, 0);
}
void GSDeviceOGL::OMSetFBO(GLuint fbo)
{
if (GLState::fbo != fbo)
{
GLState::fbo = fbo;
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
}
}
void GSDeviceOGL::OMSetDepthStencilState(GSDepthStencilOGL* dss)
{
dss->SetupDepth();
dss->SetupStencil();
}
void GSDeviceOGL::OMSetColorMaskState(OMColorMaskSelector sel)
{
if (sel.wrgba != GLState::wrgba)
{
GLState::wrgba = sel.wrgba;
glColorMaski(0, sel.wr, sel.wg, sel.wb, sel.wa);
}
}
void GSDeviceOGL::OMUnbindTexture(GSTextureOGL* tex)
{
if (GLState::rt != tex && GLState::ds_as_rt != tex && GLState::ds != tex)
return;
OMSetFBO(m_fbo);
if (GLState::rt == tex)
OMAttachRt();
if (GLState::ds_as_rt == tex)
OMAttachDsAsRt();
if (GLState::ds == tex)
OMAttachDs();
}
void GSDeviceOGL::OMSetBlendState(bool enable, GLenum src_factor, GLenum dst_factor, GLenum op,
GLenum src_factor_alpha, GLenum dst_factor_alpha, bool is_constant, u8 constant)
{
if (enable)
{
if (!GLState::blend)
{
GLState::blend = true;
glEnable(GL_BLEND);
}
if (is_constant && GLState::bf != constant)
{
GLState::bf = constant;
const float bf = static_cast<float>(constant) / 128.0f;
glBlendColor(bf, bf, bf, bf);
}
if (GLState::eq_RGB != op)
{
GLState::eq_RGB = op;
glBlendEquationSeparate(op, GL_FUNC_ADD);
}
if (GLState::f_sRGB != src_factor || GLState::f_dRGB != dst_factor ||
GLState::f_sA != src_factor_alpha || GLState::f_dA != dst_factor_alpha)
{
GLState::f_sRGB = src_factor;
GLState::f_dRGB = dst_factor;
GLState::f_sA = src_factor_alpha;
GLState::f_dA = dst_factor_alpha;
glBlendFuncSeparate(src_factor, dst_factor, src_factor_alpha, dst_factor_alpha);
}
}
else
{
if (GLState::blend)
{
GLState::blend = false;
glDisable(GL_BLEND);
}
}
}
void GSDeviceOGL::OMSetRenderTargets(GSTexture* rt, GSTexture* ds_as_rt, GSTexture* ds, const GSVector4i* scissor)
{
const bool rt_changed = (rt != GLState::rt);
const bool ds_as_rt_changed = (ds_as_rt != GLState::ds_as_rt);
const bool ds_changed = (ds != GLState::ds);
const u32 draw_buffers = GLState::draw_buffers;
g_perfmon.Put(GSPerfMon::RenderPasses, static_cast<double>(rt_changed || ds_as_rt_changed || ds_changed));
// Split up to avoid unbind/bind calls when clearing.
OMSetFBO(m_fbo);
GLState::rt_written = false;
GLState::ds_as_rt_written = false;
GLState::ds_written = false;
if (rt)
{
OMAttachRt(rt);
CommitClear(rt, false);
GLState::rt_written = rt_changed;
}
else
OMAttachRt();
if (ds_as_rt)
{
OMAttachDsAsRt(ds_as_rt);
CommitClear(ds_as_rt, false);
GLState::ds_as_rt_written = ds_as_rt_changed;
}
else
OMAttachDsAsRt();
if (ds)
{
OMAttachDs(ds);
CommitClear(ds, false);
GLState::ds_written = ds_changed;
}
else
OMAttachDs();
if (rt || ds_as_rt || ds)
{
const GSVector2i size = rt ? rt->GetSize() : (ds_as_rt ? ds_as_rt->GetSize() : ds->GetSize());
SetViewport(size);
SetScissor(scissor ? *scissor : GSVector4i::loadh(size));
}
if (draw_buffers != GLState::UpdateDrawBuffers())
{
// Always write to the first and second buffer
static constexpr GLenum target[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
glDrawBuffers(GLState::draw_buffers, target);
}
}
void GSDeviceOGL::SetViewport(const GSVector2i& viewport)
{
if (GLState::viewport != viewport)
{
GLState::viewport = viewport;
glViewport(0, 0, viewport.x, viewport.y);
}
}
void GSDeviceOGL::SetScissor(const GSVector4i& scissor)
{
if (!GLState::scissor.eq(scissor))
{
GLState::scissor = scissor;
glScissor(scissor.x, scissor.y, scissor.width(), scissor.height());
}
}
void GSDeviceOGL::SetupPipeline(const ProgramSelector& psel)
{
auto it = m_programs.find(psel);
if (it != m_programs.end())
{
it->second.Bind();
return;
}
const std::string vs(GetVSSource(psel.vs));
const std::string ps(GetPSSource(psel.ps));
GLProgram prog;
m_shader_cache.GetProgram(&prog, vs, ps);
it = m_programs.emplace(psel, std::move(prog)).first;
it->second.Bind();
}
void GSDeviceOGL::SetupSampler(PSSamplerSelector ssel)
{
PSSetSamplerState(m_ps_ss[ssel.key]);
}
GLuint GSDeviceOGL::GetPaletteSamplerID()
{
return m_palette_ss;
}
void GSDeviceOGL::SetupOM(OMDepthStencilSelector dssel)
{
OMSetDepthStencilState(m_om_dss[dssel.key]);
}
// clang-format off
static constexpr std::array<GLenum, 16> s_gl_blend_factors = { {
GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_DST_COLOR, GL_ONE_MINUS_DST_COLOR,
GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA,
GL_CONSTANT_COLOR, GL_ONE_MINUS_CONSTANT_COLOR, GL_ONE, GL_ZERO
} };
static constexpr std::array<GLenum, 3> s_gl_blend_ops = { {
GL_FUNC_ADD, GL_FUNC_SUBTRACT, GL_FUNC_REVERSE_SUBTRACT
} };
// clang-format on
void GSDeviceOGL::DoRenderHW(GSHWDrawConfig& config)
{
if (!GLState::scissor.eq(config.scissor))
{
glScissor(config.scissor.x, config.scissor.y, config.scissor.width(), config.scissor.height());
GLState::scissor = config.scissor;
}
if (config.tex && (m_features.texture_barrier || (config.tex != config.rt)))
CommitClear(config.tex, true);
if (config.pal)
CommitClear(config.pal, true);
const GSVector2i rtsize = (config.rt ? config.rt : config.ds)->GetSize();
GSTexture* colclip_rt = g_gs_device->GetColorClipTexture();
GSTexture* draw_rt = config.rt;
GSTexture* draw_ds = config.ds;
GSTexture* draw_ds_as_rt = m_ds_as_rt;
GSTexture* draw_rt_clone = nullptr;
GSTexture* draw_ds_clone = nullptr;
GSTexture* primid_texture = nullptr;
ScopedGuard recycle_temp_textures([&]() {
if (draw_rt_clone)
Recycle(draw_rt_clone);
if (draw_ds_clone)
Recycle(draw_ds_clone);
if (primid_texture)
Recycle(primid_texture);
});
if (colclip_rt)
{
if (config.colclip_mode == GSHWDrawConfig::ColClipMode::EarlyResolve)
{
const GSVector2i size = config.rt->GetSize();
const GSVector4 dRect(config.colclip_update_area);
const GSVector4 sRect = dRect / GSVector4(size.x, size.y).xyxy();
StretchRect(colclip_rt, sRect, config.rt, dRect, ShaderConvert::COLCLIP_RESOLVE, Nearest);
Recycle(colclip_rt);
g_gs_device->SetColorClipTexture(nullptr);
colclip_rt = nullptr;
}
else
{
config.ps.colclip_hw = 1;
}
}
if (config.ps.colclip_hw)
{
if (!colclip_rt)
{
config.colclip_update_area = config.drawarea;
colclip_rt = CreateFeedbackTarget(rtsize.x, rtsize.y, GSTexture::Format::ColorClip, false);
if (!colclip_rt)
{
Console.Warning("GL: Failed to allocate ColorClip render target, aborting draw.");
return;
}
OMSetRenderTargets(colclip_rt, nullptr, config.ds, nullptr);
g_gs_device->SetColorClipTexture(colclip_rt);
const GSVector4 dRect = GSVector4((config.colclip_mode == GSHWDrawConfig::ColClipMode::ConvertOnly) ? GSVector4i::loadh(rtsize) : config.drawarea);
const GSVector4 sRect = dRect / GSVector4(rtsize.x, rtsize.y).xyxy();
StretchRect(config.rt, sRect, colclip_rt, dRect, ShaderConvert::COLCLIP_INIT, Nearest);
}
draw_rt = colclip_rt ? colclip_rt : config.rt;
}
// Destination Alpha Setup
const bool need_barrier = config.require_one_barrier || (config.require_full_barrier && m_features.feedback_loops());
switch (config.destination_alpha)
{
case GSHWDrawConfig::DestinationAlphaMode::Off:
case GSHWDrawConfig::DestinationAlphaMode::Full:
break; // No setup
case GSHWDrawConfig::DestinationAlphaMode::PrimIDTracking:
primid_texture = InitPrimDateTexture(colclip_rt ? colclip_rt : config.rt, config.drawarea, config.datm);
if (!primid_texture)
{
Console.Warning("GL: Failed to allocate DATE image, aborting draw.");
return;
}
break;
case GSHWDrawConfig::DestinationAlphaMode::StencilOne:
if (need_barrier)
{
// Cleared after RT bind.
break;
}
[[fallthrough]];
case GSHWDrawConfig::DestinationAlphaMode::Stencil:
SetupDATE(colclip_rt ? colclip_rt : config.rt, config.ds, config.datm, config.drawarea);
break;
}
IASetVertexBuffer(config.verts, config.nverts, GetVertexAlignment(config.vs.expand));
if (config.vs.UseFixedExpandIndexBuffer())
{
IASetVAO(m_expand_vao);
m_index.start = 0;
m_index.count = config.nindices;
}
else if (config.vs.UseVSExpandIndexBuffer())
{
IASetVAO(m_dummy_vao); // Unbind vertex buffer from IA to prevent unwanted fetches.
VSSetIndexBuffer(config.indices, config.nindices);
}
else
{
IASetVAO(m_vao);
IASetIndexBuffer(config.indices, config.nindices);
}
GLenum topology = 0;
switch (config.topology)
{
case GSHWDrawConfig::Topology::Point: topology = GL_POINTS; break;
case GSHWDrawConfig::Topology::Line: topology = GL_LINES; break;
case GSHWDrawConfig::Topology::Triangle: topology = GL_TRIANGLES; break;
}
IASetPrimitiveTopology(topology);
if (config.tex && (m_features.texture_barrier || config.tex_hazard == GSHWDrawConfig::TEX_HAZARD_NONE))
PSSetShaderResource(TEXTURE_TEXTURE, config.tex);
if (config.pal)
PSSetShaderResource(TEXTURE_PALETTE, config.pal);
if (m_features.texture_barrier && (config.require_one_barrier || config.require_full_barrier))
PSSetShaderResource(TEXTURE_RT, colclip_rt ? colclip_rt : config.rt);
if (m_features.texture_barrier && (config.require_one_barrier || config.require_full_barrier) && config.ps.IsFeedbackLoopDepth())
// With ARM depth-stencil fetch the shader reads gl_LastFragDepthARM, not a
// sampler, so don't bind the live depth attachment as a texture (avoids a
// feedback-loop bind the driver may flag).
PSSetShaderResource(TEXTURE_DEPTH, (m_features.depth_feedback && !m_arm_depth_fetch) ? config.ds : m_ds_as_rt);
SetupSampler(config.sampler);
if (m_vs_cb_cache.Update(config.cb_vs))
VSSetUniformBuffer(m_vs_cb_cache);
if (m_ps_cb_cache.Update(config.cb_ps))
PSSetUniformBuffer(m_ps_cb_cache);
ProgramSelector psel;
psel.vs = config.vs;
psel.ps.key_hi = config.ps.key_hi;
psel.ps.key_lo = config.ps.key_lo;
std::memset(psel.pad, 0, sizeof(psel.pad));
SetupPipeline(psel);
// In Time Crisis:
// 1. Fullscreen sprite reads depth and writes alpha (rt_hazard_barrier true from config.ds == config.tex)
// 2. Fullscreen sprite writes gray, rta hw blend blends based on dst alpha.
// On Nvidia, 2 seems to not pick up the data written by 1 unless we add a second barrier.
// Pretty sure GL is supposed to guarantee that the blend unit is coherent with previous pixel write out, so calling this a bug.
bool broken_blend_coherency_barrier = false;
if (m_bugs.broken_blend_coherency)
broken_blend_coherency_barrier = (config.IsFeedbackLoopRT(psel.ps) || psel.ps.blend_c == 1) && GLState::rt == config.rt;
if (config.require_one_barrier || !m_features.texture_barrier)
{
broken_blend_coherency_barrier = false;
}
// additional non-pipeline config stuff
const bool point_size_enabled = config.vs.point_size;
if (GLState::point_size != point_size_enabled)
{
if (point_size_enabled)
glEnable(GL_PROGRAM_POINT_SIZE);
else
glDisable(GL_PROGRAM_POINT_SIZE);
GLState::point_size = point_size_enabled;
}
if (config.topology == GSHWDrawConfig::Topology::Line)
{
const float line_width = config.line_expand ? config.cb_ps.ScaleFactor.z : 1.0f;
if (GLState::line_width != line_width)
{
GLState::line_width = line_width;
glLineWidth(line_width);
}
}
if (primid_texture)
{
GL_PUSH("Destination Alpha PrimID Init");
OMSetRenderTargets(primid_texture, nullptr, config.ds, &config.scissor);
OMColorMaskSelector mask;
mask.wrgba = 0;
mask.wr = true;
OMSetColorMaskState(mask);
OMSetBlendState(true, GL_ONE, GL_ONE, GL_MIN);
OMDepthStencilSelector dss = config.depth;
dss.zwe = 0; // Don't write depth
SetupOM(dss);
// Compute primitiveID max that pass the date test (Draw without barrier)
Draw(config);
psel.ps.date = 3;
config.alpha_second_pass.ps.date = 3;
SetupPipeline(psel);
PSSetShaderResource(TEXTURE_PRIMID, primid_texture);
}
if (draw_ds_as_rt)
{
// We must clear the blend equation of any dual source blending factors or
// it may interact badly with MRTs, even if blending is disabled.
OMSetBlendState(true);
}
if (config.blend.IsEffective(config.colormask))
{
OMSetBlendState(config.blend.enable, s_gl_blend_factors[config.blend.src_factor],
s_gl_blend_factors[config.blend.dst_factor], s_gl_blend_ops[config.blend.op],
s_gl_blend_factors[config.blend.src_factor_alpha], s_gl_blend_factors[config.blend.dst_factor_alpha],
config.blend.constant_enable, config.blend.constant);
}
else
{
OMSetBlendState();
}
// Clear texture binding when it's bound to RT or DS.
if (!config.tex && ((draw_rt && static_cast<GSTextureOGL*>(draw_rt)->GetID() == GLState::tex_unit[0]) ||
(draw_ds && static_cast<GSTextureOGL*>(draw_ds)->GetID() == GLState::tex_unit[0])))
PSSetShaderResource(TEXTURE_TEXTURE, nullptr);
// Avoid changing framebuffer just to switch from rt+depth to rt and vice versa.
bool fb_optimization_needs_barrier = false;
if (!draw_rt && GLState::rt && GLState::ds == draw_ds && config.tex != GLState::rt &&
draw_ds && GLState::rt->GetSize() == draw_ds->GetSize() && !draw_ds_as_rt)
{
draw_rt = GLState::rt;
fb_optimization_needs_barrier = !GLState::rt_written;
}
else if (!draw_ds && GLState::ds && GLState::rt == draw_rt && config.tex != GLState::ds &&
draw_rt && GLState::ds->GetSize() == draw_rt->GetSize() && !draw_ds_as_rt)
{
draw_ds = GLState::ds;
fb_optimization_needs_barrier = !GLState::ds_written;
}
// Be careful of the rt already being bound and the blend using the RT without a barrier.
if (fb_optimization_needs_barrier && broken_blend_coherency_barrier)
{
// Ensure all depth writes are finished before sampling
GL_INS("GL: Texture barrier to flush depth or rt before reading");
g_perfmon.Put(GSPerfMon::Barriers, 1);
glTextureBarrier();
}
const bool rt_feedbackloop_pass1 = config.IsFeedbackLoopRT(config.ps);
const bool rt_feedbackloop_pass2 = config.IsFeedbackLoopRT(config.alpha_second_pass.ps);
if (draw_rt && !m_features.texture_barrier && (((config.require_one_barrier || (config.require_full_barrier && m_features.multidraw_fb_copy)) &&
(rt_feedbackloop_pass1 || rt_feedbackloop_pass2))))
{
// Requires a copy of the RT.
draw_rt_clone = CreateTexture(rtsize.x, rtsize.y, 1, draw_rt->GetFormat(), true);
if (!draw_rt_clone)
Console.Warning("GL: Failed to allocate temp texture for RT copy.");
}
const bool ds_feedbackloop_pass1 = config.ps.IsFeedbackLoopDepth();
const bool ds_feedbackloop_pass2 = config.alpha_second_pass.ps.IsFeedbackLoopDepth();
if (draw_ds && !m_features.texture_barrier && m_features.depth_feedback &&
(config.require_one_barrier || (config.require_full_barrier && m_features.multidraw_fb_copy)) && (ds_feedbackloop_pass1 || ds_feedbackloop_pass2))
{
// Requires a copy of the DS.
draw_ds_clone = CreateTexture(rtsize.x, rtsize.y, 1, draw_ds->GetFormat(), true);
if (!draw_ds_clone)
Console.Warning("GL: Failed to allocate temp texture for DS copy.");
}
OMSetRenderTargets(draw_rt, draw_ds_as_rt, draw_ds, &config.scissor);
OMSetColorMaskState(config.colormask);
SetupOM(config.depth);
// Clear stencil as close as possible to the RT bind, to avoid framebuffer swaps.
if (config.destination_alpha == GSHWDrawConfig::DestinationAlphaMode::StencilOne && need_barrier)
{
constexpr GLint clear_color = 1;
glClearBufferiv(GL_STENCIL, 0, &clear_color);
}
SendHWDraw(config, rt_feedbackloop_pass1 ? draw_rt_clone : nullptr, draw_rt, ds_feedbackloop_pass1 ? draw_ds_clone : nullptr, draw_ds,
config.require_one_barrier, config.require_full_barrier);
if (config.blend_multi_pass.enable)
{
if (config.blend.IsEffective(config.colormask))
{
OMSetBlendState(config.blend_multi_pass.blend.enable, s_gl_blend_factors[config.blend_multi_pass.blend.src_factor],
s_gl_blend_factors[config.blend_multi_pass.blend.dst_factor], s_gl_blend_ops[config.blend_multi_pass.blend.op],
s_gl_blend_factors[config.blend_multi_pass.blend.src_factor_alpha], s_gl_blend_factors[config.blend_multi_pass.blend.dst_factor_alpha],
config.blend_multi_pass.blend.constant_enable, config.blend_multi_pass.blend.constant);
}
else
{
OMSetBlendState();
}
psel.ps.no_color1 = config.blend_multi_pass.no_color1;
psel.ps.blend_hw = config.blend_multi_pass.blend_hw;
psel.ps.dither = config.blend_multi_pass.dither;
SetupPipeline(psel);
Draw(config);
}
if (config.alpha_second_pass.enable)
{
// cbuffer will definitely be dirty if aref changes, no need to check it
if (config.cb_ps.FogColor_AREF.a != config.alpha_second_pass.ps_aref)
{
config.cb_ps.FogColor_AREF.a = config.alpha_second_pass.ps_aref;
PSSetUniformBuffer(config.cb_ps);
}
psel.ps = config.alpha_second_pass.ps;
SetupPipeline(psel);
OMSetColorMaskState(config.alpha_second_pass.colormask);
if (config.blend.IsEffective(config.alpha_second_pass.colormask))
{
OMSetBlendState(config.blend.enable, s_gl_blend_factors[config.blend.src_factor],
s_gl_blend_factors[config.blend.dst_factor], s_gl_blend_ops[config.blend.op],
s_gl_blend_factors[config.blend.src_factor_alpha], s_gl_blend_factors[config.blend.dst_factor_alpha],
config.blend.constant_enable, config.blend.constant);
}
else
{
OMSetBlendState();
}
const bool one_barrier = config.alpha_second_pass.require_one_barrier && m_features.feedback_loops();
SetupOM(config.alpha_second_pass.depth);
SendHWDraw(config, rt_feedbackloop_pass2 ? draw_rt_clone : nullptr, draw_rt, ds_feedbackloop_pass2 ? draw_ds_clone : nullptr, draw_ds,
one_barrier, config.alpha_second_pass.require_full_barrier);
}
if (colclip_rt)
{
config.colclip_update_area = config.colclip_update_area.runion(config.drawarea);
if ((config.colclip_mode == GSHWDrawConfig::ColClipMode::ResolveOnly || config.colclip_mode == GSHWDrawConfig::ColClipMode::ConvertAndResolve))
{
const GSVector2i size = config.rt->GetSize();
const GSVector4 dRect(config.colclip_update_area);
const GSVector4 sRect = dRect / GSVector4(size.x, size.y).xyxy();
StretchRect(colclip_rt, sRect, config.rt, dRect, ShaderConvert::COLCLIP_RESOLVE, Nearest);
Recycle(colclip_rt);
g_gs_device->SetColorClipTexture(nullptr);
}
}
}
void GSDeviceOGL::FeedbackCopyAndBind(const GSHWDrawConfig& config,
GSTexture* rt, GSTexture* rt_clone, GSTexture* ds, GSTexture* ds_clone, const GSVector4i& copyarea)
{
if (rt_clone)
{
DoCopyRect(rt, rt_clone, copyarea, copyarea.left, copyarea.top);
PSSetShaderResource(2, rt_clone);
if (config.tex_hazard == GSHWDrawConfig::TEX_HAZARD_RT)
PSSetShaderResource(0, rt_clone);
}
if (ds_clone)
{
DoCopyRect(ds, ds_clone, copyarea, copyarea.left, copyarea.top);
PSSetShaderResource(4, ds_clone);
if (config.tex_hazard == GSHWDrawConfig::TEX_HAZARD_DEPTH)
PSSetShaderResource(0, ds_clone);
}
}
// Choose the best copy area based on the hazards and whether we need RT and/or DS copies.
void GSDeviceOGL::FeedbackCopyAndBind(const GSHWDrawConfig& config,
GSTexture* rt, GSTexture* rt_clone, GSTexture* ds, GSTexture* ds_clone,
const GSVector4i& copyarea, const GSVector4i& samplearea)
{
const GSVector4i rtsize = (rt ? rt : ds)->GetRect();
if (config.tex_hazard != GSHWDrawConfig::TEX_HAZARD_NONE)
{
const GSVector4i union_rect = config.drawarea.runion(config.samplearea);
const u32 size_union = union_rect.width() * union_rect.height();
const u32 size_indiv = config.drawarea.width() * config.drawarea.height() +
config.samplearea.width() * config.samplearea.height();
// Do an individual copy if the union is larger than the sum of individual areas.
if (size_union > size_indiv)
{
FeedbackCopyAndBind(config, rt, rt_clone, ds, ds_clone, ProcessCopyArea(rtsize, config.drawarea));
FeedbackCopyAndBind(config, rt, rt_clone, ds, ds_clone, ProcessCopyArea(rtsize, config.samplearea));
}
else
{
FeedbackCopyAndBind(config, rt, rt_clone, ds, ds_clone, ProcessCopyArea(rtsize, union_rect));
}
}
else
{
// No RT/DS hazards so just need the draw area.
FeedbackCopyAndBind(config, rt, rt_clone, ds, ds_clone, ProcessCopyArea(rtsize, config.drawarea));
}
}
void GSDeviceOGL::SendHWDraw(const GSHWDrawConfig& config,
GSTexture* draw_rt_clone, GSTexture* draw_rt, GSTexture* draw_ds_clone, GSTexture* draw_ds,
const bool one_barrier, const bool full_barrier)
{
#ifdef PCSX2_DEVBUILD
if ((one_barrier || full_barrier) && !(config.IsFeedbackLoopRT(config.ps) || config.IsFeedbackLoopDepth(config.ps))) [[unlikely]]
Console.Warning("OpenGL: Possible unnecessary barrier detected.");
#endif
if (full_barrier)
{
pxAssert(config.drawlist && !config.drawlist->empty());
const u32 indices_per_prim = config.indices_per_prim;
const u32 draw_list_size = static_cast<u32>(config.drawlist->size());
if (m_features.texture_barrier)
g_perfmon.Put(GSPerfMon::Barriers, static_cast<u32>(draw_list_size));
else
pxAssert(config.drawlist_bbox && static_cast<u32>(config.drawlist_bbox->size()) == draw_list_size);
if (!m_features.texture_barrier && config.tex_hazard != config.TEX_HAZARD_NONE)
FeedbackCopyAndBind(config, draw_rt, draw_rt_clone, draw_ds, draw_ds_clone, config.samplearea);
for (u32 n = 0, p = 0; n < draw_list_size; n++)
{
const u32 count = config.drawlist->at(n) * indices_per_prim;
if (m_features.texture_barrier)
{
glTextureBarrier();
}
else
{
const GSVector4i bbox = config.drawlist_bbox->at(n).rintersect(config.drawarea);
FeedbackCopyAndBind(config, draw_rt, draw_rt_clone, draw_ds, draw_ds_clone, bbox);
}
Draw(config, p, count);
p += count;
}
return;
}
if (one_barrier)
{
if (m_features.texture_barrier)
{
g_perfmon.Put(GSPerfMon::Barriers, 1);
glTextureBarrier();
}
else
{
// Optimization: For alpha second pass we can reuse the copy snapshot from the first pass.
FeedbackCopyAndBind(config, draw_rt, draw_rt_clone, draw_ds, draw_ds_clone, config.drawarea, config.samplearea);
}
}
Draw(config);
}
// Note: used as a callback of DebugMessageCallback. Don't change the signature
void GSDeviceOGL::DebugMessageCallback(GLenum gl_source, GLenum gl_type, GLuint id, GLenum gl_severity, GLsizei gl_length, const GLchar* gl_message, const void* userParam)
{
std::string message(gl_message, gl_length >= 0 ? gl_length : strlen(gl_message));
std::string type, severity, source;
switch (gl_type)
{
case GL_DEBUG_TYPE_ERROR_ARB : type = "Error"; break;
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_ARB : type = "Deprecated bhv"; break;
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_ARB : type = "Undefined bhv"; break;
case GL_DEBUG_TYPE_PORTABILITY_ARB : type = "Portability"; break;
case GL_DEBUG_TYPE_PERFORMANCE_ARB : type = "Perf"; break;
case GL_DEBUG_TYPE_OTHER_ARB : type = "Oth"; break;
case GL_DEBUG_TYPE_PUSH_GROUP : return; // Don't print message injected by myself
case GL_DEBUG_TYPE_POP_GROUP : return; // Don't print message injected by myself
default : type = "TTT"; break;
}
switch (gl_severity)
{
case GL_DEBUG_SEVERITY_HIGH_ARB : severity = "High"; break;
case GL_DEBUG_SEVERITY_MEDIUM_ARB : severity = "Mid"; break;
case GL_DEBUG_SEVERITY_LOW_ARB : severity = "Low"; break;
default:
if (id == 0xFEAD)
severity = "Cache";
else if (id == 0xB0B0)
severity = "REG";
else if (id == 0xD0D0)
severity = "EXTRA";
break;
}
switch (gl_source)
{
case GL_DEBUG_SOURCE_API_ARB : source = "API"; break;
case GL_DEBUG_SOURCE_WINDOW_SYSTEM_ARB : source = "WINDOW"; break;
case GL_DEBUG_SOURCE_SHADER_COMPILER_ARB : source = "COMPILER"; break;
case GL_DEBUG_SOURCE_THIRD_PARTY_ARB : source = "3rdparty"; break;
case GL_DEBUG_SOURCE_APPLICATION_ARB : source = "Application"; break;
case GL_DEBUG_SOURCE_OTHER_ARB : source = "Others"; break;
default : source = "???"; break;
}
// Don't spam noisy information on the terminal
if (gl_severity != GL_DEBUG_SEVERITY_NOTIFICATION && gl_source != GL_DEBUG_SOURCE_APPLICATION)
{
Console.Error("T:%s\tID:%d\tS:%s\t=> %s", type.c_str(), g_gs_renderer ? g_gs_renderer->s_n : 0, severity.c_str(), message.c_str());
}
}
#ifdef ENABLE_OGL_DEBUG
static int s_debugGroupDepth = 0;
#endif
void GSDeviceOGL::PushDebugGroup(const char* fmt, ...)
{
#ifdef ENABLE_OGL_DEBUG
if (!glPushDebugGroup || !GSConfig.UseDebugDevice)
return;
std::va_list ap;
va_start(ap, fmt);
const std::string buf(StringUtil::StdStringFromFormatV(fmt, ap));
va_end(ap);
if (!buf.empty())
{
glPushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 0xBAD, -1, buf.c_str());
// Make sure the calls succeed first.
if (glGetError() == GL_NO_ERROR)
s_debugGroupDepth++;
}
#endif
}
void GSDeviceOGL::PopDebugGroup()
{
#ifdef ENABLE_OGL_DEBUG
if (!glPopDebugGroup || !GSConfig.UseDebugDevice || (s_debugGroupDepth <= 0))
return;
glPopDebugGroup();
s_debugGroupDepth--;
#endif
}
void GSDeviceOGL::InsertDebugMessage(DebugMessageCategory category, const char* fmt, ...)
{
#ifdef ENABLE_OGL_DEBUG
if (!glDebugMessageInsert || !GSConfig.UseDebugDevice)
return;
GLenum type, id, severity;
switch (category)
{
case GSDevice::DebugMessageCategory::Cache:
type = GL_DEBUG_TYPE_OTHER;
id = 0xFEAD;
severity = GL_DEBUG_SEVERITY_NOTIFICATION;
break;
case GSDevice::DebugMessageCategory::Reg:
type = GL_DEBUG_TYPE_OTHER;
id = 0xB0B0;
severity = GL_DEBUG_SEVERITY_NOTIFICATION;
break;
case GSDevice::DebugMessageCategory::Debug:
type = GL_DEBUG_TYPE_OTHER;
id = 0xD0D0;
severity = GL_DEBUG_SEVERITY_NOTIFICATION;
break;
case GSDevice::DebugMessageCategory::Message:
type = GL_DEBUG_TYPE_ERROR;
id = 0xDEAD;
severity = GL_DEBUG_SEVERITY_MEDIUM;
break;
case GSDevice::DebugMessageCategory::Performance:
default:
type = GL_DEBUG_TYPE_PERFORMANCE;
id = 0xFEE1;
severity = GL_DEBUG_SEVERITY_NOTIFICATION;
break;
}
std::va_list ap;
va_start(ap, fmt);
const std::string buf(StringUtil::StdStringFromFormatV(fmt, ap));
va_end(ap);
if (!buf.empty())
glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, type, id, severity, buf.size(), buf.c_str());
#endif
}