// 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/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 #include #include 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 GSDeviceOGL::GetAdapterInfo() { std::vector 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(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 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 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(renderer_name) : "OpenGL"; ai.max_texture_size = std::max(static_cast((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(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(&m_vs_cb_cache), 0xFF, sizeof(m_vs_cb_cache)); std::memset(static_cast(&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 expand_data = std::make_unique(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 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(shader.Biln())); macro += fmt::format("#define HAS_STENCIL_OUTPUT {}\n", static_cast(shader.StencilOutput())); macro += fmt::format("#define HAS_INTEGER_OUTPUT {}\n", static_cast(shader.IntegerOutputBpp() != 0)); macro += fmt::format("#define HAS_DEPTH_OUTPUT {}\n", static_cast(shader.DepthOutput())); macro += fmt::format("#define HAS_FLOAT32_INPUT {}\n", static_cast(shader.Float32Input())); macro += fmt::format("#define HAS_FLOAT32_OUTPUT {}\n", static_cast(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(shader.DepthOutput()), static_cast(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 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(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 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 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 vertex_shader = ReadShaderSource("shaders/opengl/tfx_vgs.glsl"); std::optional 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(profile_selection.driver.version.major), static_cast(profile_selection.driver.version.minor), static_cast(profile_selection.driver.version.patch), static_cast(profile_selection.driver.version.build), static_cast(profile_selection.driver.matched_rule_count), static_cast(profile_selection.driver.bugs), static_cast(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(glGetString(GL_VENDOR)))); Console.WriteLn(fmt::format("GL_VERSION: {}", gl_version_str)); Console.WriteLn(fmt::format("GL_RENDERER: {}", reinterpret_cast(glGetString(GL_RENDERER)))); Console.WriteLn(fmt::format( "GL_SHADING_LANGUAGE_VERSION: {}", reinterpret_cast(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(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; m_features.framebuffer_fetch = (GLAD_GL_ARM_shader_framebuffer_fetch || GLAD_GL_EXT_shader_framebuffer_fetch); // The Mali r44p1 blob loses the rendering context under the in-tile framebuffer-fetch blend path, // exactly as it loses the Vulkan device under attachment-feedback-loop (VK_ERROR_DEVICE_LOST on // effectively every game -- Mali-G615 r44p1). Mirror the Vulkan-side r44p1 gate (see GSDeviceVK.cpp) // and drop this one blob to the non-fetch (copy) blend path. Narrow by driver version, not vendor, // so other (working) Mali blobs keep the fast path. GL_VERSION reads e.g. "OpenGL ES 3.2 v1.r44p1-...". if (m_features.framebuffer_fetch) { const char* gl_version = reinterpret_cast(glGetString(GL_VERSION)); if (gl_version && std::strstr(gl_version, "r44p1")) { Console.WriteLn("Mali r44p1: disabling framebuffer fetch (GL context-lost workaround; matches the Vulkan gate)."); m_features.framebuffer_fetch = false; } } if (m_features.framebuffer_fetch && GSConfig.DisableFramebufferFetch) { Host::AddOSDMessage( "Framebuffer fetch was found but is disabled. This will reduce performance.", Host::OSD_ERROR_DURATION); m_features.framebuffer_fetch = false; } 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; 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; if (GetMobileGSTuning().force_partial_texture_preloading && GSConfig.TexturePreloading == TexturePreloadingLevel::Full) { GSConfig.TexturePreloading = TexturePreloadingLevel::Partial; Console.Warning("GL: Mobile GS %s/%s profile lowered texture preloading to partial.", GpuProfileDetector::RuntimeProfileToString(GetRuntimeGPUProfile()), GetMobileGPUIdentity().name.c_str()); } #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. if (GLAD_GL_ARM_shader_framebuffer_fetch) { Console.WriteLn(Color_Yellow, "GL: Applying Mali-specific optimizations for tile-based rendering."); m_features.framebuffer_fetch = true; if (GSConfig.OverrideTextureBarriers == -1) { m_features.texture_barrier = m_features.framebuffer_fetch; 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)."); } } { const bool has_arm_fetch = GLAD_GL_ARM_shader_framebuffer_fetch; const bool has_ext_fetch = GLAD_GL_EXT_shader_framebuffer_fetch; const bool has_pls_fetch = GLAD_GL_EXT_shader_pixel_local_storage; Console.WriteLn("GL: Framebuffer fetch extension caps: arm=%d ext=%d pls=%d.", has_arm_fetch ? 1 : 0, has_ext_fetch ? 1 : 0, has_pls_fetch ? 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 = "None"; if (m_features.framebuffer_fetch) { if (use_mali_profile) active_fetch_backend = "ARM"; else if (has_ext_fetch || has_pls_fetch) active_fetch_backend = "EXT/PLS"; else if (has_arm_fetch) active_fetch_backend = "ARM"; } Console.WriteLn("GL: Active framebuffer fetch backend (%s profile): %s.", active_profile_name, active_fetch_backend); if (use_mali_profile && !has_arm_fetch) Console.Warning("GL: Mali profile selected but ARM framebuffer fetch is unavailable; using non-fetch fallback."); } 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(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(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, ¤t_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(glGetString(GL_VENDOR)); const char* gl_renderer = reinterpret_cast(glGetString(GL_RENDERER)); const char* gl_version = reinterpret_cast(glGetString(GL_VERSION)); const char* gl_shading_language_version = reinterpret_cast(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(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(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(static_cast(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(static_cast(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((static_cast(m_index.start) + static_cast(offset)) * sizeof(u16)), static_cast(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((static_cast(m_index.start) + static_cast(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(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(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(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 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(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(sel.fst)) + fmt::format("#define VS_IIP {}\n", static_cast(sel.iip)) + fmt::format("#define VS_POINT_SIZE {}\n", static_cast(sel.point_size)) + fmt::format("#define VS_EXPAND {}\n", static_cast(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(sel.atst)) + fmt::format("#define PS_AFAIL {}\n", static_cast(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_ZTST {}\n", sel.ztst) + fmt::format("#define PS_AA1 {}\n", static_cast(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(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(sTex)->GetID(); const GLuint& did = static_cast(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(sTex)->GetID(), static_cast(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(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(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(dTex->GetWidth()), static_cast(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(vertex_map.pointer); u16* idx = reinterpret_cast(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(shader)].Bind(); m_interlace.ps[static_cast(shader)].Uniform4fv(0, cb.ZrH.F32); DoStretchRect(sTex, sRect, dTex, dRect, m_interlace.ps[static_cast(shader)], filter); } bool GSDeviceOGL::CompileFXAAProgram() { const std::string_view fxaa_macro = "#define FXAA_GLSL_130 1\n"; const std::optional 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 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(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(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> params; if (GetShaderChainParams(m_shader_chain_preset, ¶ms)) { libra_gl_filter_chain_t chain = static_cast(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(sTex); GSTextureOGL* const dst = static_cast(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(src->GetWidth()), static_cast(src->GetHeight())}; const libra_image_gl_t out = {dst->GetID(), dst->GetGLFormat(), static_cast(dst->GetWidth()), static_cast(dst->GetHeight())}; const libra_viewport_t vp = {0.0f, 0.0f, static_cast(dst->GetWidth()), static_cast(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(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 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(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& buffer, const void* index, size_t count) { const u32 size = static_cast(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(std::size(GLState::tex_unit))); const GLuint id = sr ? static_cast(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 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& 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(&constants[8])); PSSetShaderResource(TEXTURE_TEXTURE, sTex); glBindImageTexture(0, static_cast(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 glsl = ReadShaderSource("shaders/opengl/imgui.glsl"); if (!glsl.has_value()) { Console.Error("GL: Failed to read imgui.glsl"); return false; } std::optional 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(m_window_info.surface_width); constexpr float T = 0.0f; const float B = static_cast(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(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(&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(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(rt); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, rt ? static_cast(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(ds_as_rt); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, ds_as_rt ? static_cast(ds_as_rt)->GetID() : 0, 0); } void GSDeviceOGL::OMAttachDs(GSTexture* ds) { if (GLState::ds == ds) return; GLState::ds = static_cast(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(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(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(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 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 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(draw_rt)->GetID() == GLState::tex_unit[0]) || (draw_ds && static_cast(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(config.drawlist->size()); if (m_features.texture_barrier) g_perfmon.Put(GSPerfMon::Barriers, static_cast(draw_list_size)); else pxAssert(config.drawlist_bbox && static_cast(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 }