From 7e8c32a063f4c72f234d76bc9224bd112bf412fb Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 30 Jul 2026 22:59:28 -0400 Subject: [PATCH] [Feat] (MG_Backend, MG_Impl): expose experimental GL 4.6 CTS limits --- MobileGL/MG_Backend/BackendObject.h | 7 ++ .../DirectGLES/BackendObject_DirectGLES.cpp | 21 +++- .../BackendObject_DirectVulkan.cpp | 20 +++- MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 30 ++++++ .../BackendLoader/BackendLoaderTest.cpp | 75 ++++++++++++++ MobileGL/MG_Test/SanityTest.cpp | 98 +++++++++++++++++-- .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 46 +++++++++ .../MG_Util/BackendLoaders/OpenGL/Loader.h | 6 ++ .../MG_Util/BackendLoaders/Vulkan/Loader.cpp | 22 +++++ .../MG_Util/BackendLoaders/Vulkan/Loader.h | 3 + 10 files changed, 320 insertions(+), 8 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 1167ab0d..d641dcfa 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -300,6 +300,13 @@ namespace MobileGL { Float ViewportBoundsRangeMin = 0.0f; Float ViewportBoundsRangeMax = 0.0f; Int ViewportSubpixelBits = 0; + // GL 4.x fragment-interpolation offset limits. These defaults are the + // core minimums and are replaced by live GLES/Vulkan device limits. + Float MinFragmentInterpolationOffset = -0.5f; + // For four fractional bits the greatest required legal offset is + // 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70). + Float MaxFragmentInterpolationOffset = 0.4375f; + Int FragmentInterpolationOffsetBits = 4; Bool SupportsWideLines = false; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; Uint32 SubgroupSize = 0; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 1baa3541..ace7ce05 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include namespace MobileGL::MG_Backend::DirectGLES { @@ -603,7 +604,7 @@ namespace MobileGL::MG_Backend::DirectGLES { .ExtraVendor = Nullopt, // Extra vendor .RendererGLInfo = { - .TargetGLVersion = {3, 3, 0}, // Target OpenGL Version + .TargetGLVersion = {4, 6, 0}, // Experimental GL CTS target version .TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version // Baseline advertisement (no timer queries / anisotropy yet); reconciled // once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions. @@ -1034,6 +1035,24 @@ namespace MobileGL::MG_Backend::DirectGLES { m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin; m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax; m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits; + m_dynamicParameters.MinFragmentInterpolationOffset = + std::isfinite(m_GLESCapabilities.MinFragmentInterpolationOffset) && + m_GLESCapabilities.MinFragmentInterpolationOffset <= -0.5f + ? m_GLESCapabilities.MinFragmentInterpolationOffset + : -0.5f; + m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f; + m_dynamicParameters.FragmentInterpolationOffsetBits = 4; + if (m_GLESCapabilities.FragmentInterpolationOffsetBits >= 4 && + std::isfinite(m_GLESCapabilities.MaxFragmentInterpolationOffset)) { + const Float requiredMaxOffset = + 0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits); + if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) { + m_dynamicParameters.MaxFragmentInterpolationOffset = + m_GLESCapabilities.MaxFragmentInterpolationOffset; + m_dynamicParameters.FragmentInterpolationOffsetBits = + m_GLESCapabilities.FragmentInterpolationOffsetBits; + } + } m_dynamicParameters.SupportsWideLines = m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f; diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 4723c769..789957b6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -18,6 +18,7 @@ #include "MG_Util/Texture/TextureFormatProcessor.h" #include +#include #include #include @@ -505,7 +506,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { .ExtraVendor = Nullopt, .RendererGLInfo = { - .TargetGLVersion = {3, 3, 0}, + .TargetGLVersion = {4, 6, 0}, // Experimental GL CTS target version .TargetGLSLVersion = {4, 6, 0}, // Baseline advertisement (no shader subgroup, no timer queries); a // live backend reconciles its copy in UpdateAdvertisedExtensions. @@ -796,6 +797,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin; m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax; m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits; + m_dynamicParameters.MinFragmentInterpolationOffset = + std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) && + m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f + ? m_vulkanCaps.MinFragmentInterpolationOffset + : -0.5f; + m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f; + m_dynamicParameters.FragmentInterpolationOffsetBits = 4; + if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 && + std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) { + const Float requiredMaxOffset = + 0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits); + if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) { + m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset; + m_dynamicParameters.FragmentInterpolationOffsetBits = + m_vulkanCaps.FragmentInterpolationOffsetBits; + } + } m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines; m_dynamicParameters.MaxShaderStorageBlockSize = std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize); diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 74eac404..a0d5f397 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -465,6 +465,14 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_STENCIL_TEST: *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE; return; + case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET: + case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET: + case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: { + GLfloat value = 0.0f; + GetFloatv(pname, &value); + *params = value != 0.0f ? GL_TRUE : GL_FALSE; + return; + } default: break; } @@ -520,6 +528,19 @@ namespace MobileGL::MG_Impl::GLImpl { params[1] = dynamicParameters.ViewportBoundsRangeMax; return; } + case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET: + case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET: + case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: { + const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); + if (pname == GL_MIN_FRAGMENT_INTERPOLATION_OFFSET) { + params[0] = dynamicParameters.MinFragmentInterpolationOffset; + } else if (pname == GL_MAX_FRAGMENT_INTERPOLATION_OFFSET) { + params[0] = dynamicParameters.MaxFragmentInterpolationOffset; + } else { + params[0] = static_cast(dynamicParameters.FragmentInterpolationOffsetBits); + } + return; + } case GL_DEPTH_CLEAR_VALUE: params[0] = MG_State::pGLContext->GetClearDepth(); return; @@ -1957,6 +1978,15 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_SUBPIXEL_BITS: *params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits); break; + case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET: + *params = static_cast(std::lround(dynamicParameters.MinFragmentInterpolationOffset)); + break; + case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET: + *params = static_cast(std::lround(dynamicParameters.MaxFragmentInterpolationOffset)); + break; + case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: + *params = dynamicParameters.FragmentInterpolationOffsetBits; + break; case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT: *params = static_cast(dynamicParameters.UniformBufferOffsetAlignment); break; diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp index 7af460a5..9ba15f6a 100644 --- a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -34,6 +34,11 @@ namespace { GLint maxFragmentImageUniforms = 4; GLint maxComputeImageUniforms = 5; bool maxGeometryImageUniformsQueried = false; + GLfloat minFragmentInterpolationOffset = -0.75f; + GLfloat maxFragmentInterpolationOffset = 0.625f; + GLint fragmentInterpolationOffsetBits = 6; + bool fragmentInterpolationLimitsQueried = false; + bool fragmentInterpolationQueryRaisesError = false; // Emulates ANGLE-on-Vulkan: the draw reads the indirect command's // baseInstance word and exposes it through gl_InstanceID. bool drawLeaksBaseInstanceWord = false; @@ -108,6 +113,14 @@ namespace { case GL_MAX_COMPUTE_IMAGE_UNIFORMS: *data = g_fake.maxComputeImageUniforms; break; + case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: + g_fake.fragmentInterpolationLimitsQueried = true; + if (g_fake.fragmentInterpolationQueryRaisesError) { + g_fake.pendingError = GL_INVALID_ENUM; + } else { + *data = g_fake.fragmentInterpolationOffsetBits; + } + break; // FillInGLESCapabilities reads the context version before running the // baseInstance probe, which requires ES >= 3.1. case GL_MAJOR_VERSION: @@ -155,6 +168,22 @@ namespace { g_fake.maxTextureMaxAnisotropyQueried = true; data[0] = g_fake.maxTextureMaxAnisotropy; break; + case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET: + g_fake.fragmentInterpolationLimitsQueried = true; + if (g_fake.fragmentInterpolationQueryRaisesError) { + g_fake.pendingError = GL_INVALID_ENUM; + } else { + data[0] = g_fake.minFragmentInterpolationOffset; + } + break; + case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET: + g_fake.fragmentInterpolationLimitsQueried = true; + if (g_fake.fragmentInterpolationQueryRaisesError) { + g_fake.pendingError = GL_INVALID_ENUM; + } else { + data[0] = g_fake.maxFragmentInterpolationOffset; + } + break; // Two-component range queries. case GL_ALIASED_LINE_WIDTH_RANGE: case GL_SMOOTH_LINE_WIDTH_RANGE: @@ -462,6 +491,52 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried); } +TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) { + const auto funcs = MakeFakeGLESFunctions(); + + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + MobileGL::MG_External::GLESCapabilities unsupportedCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(unsupportedCaps, funcs)); + EXPECT_FALSE(unsupportedCaps.SupportsShaderMultisampleInterpolation); + EXPECT_FALSE(g_fake.fragmentInterpolationLimitsQueried); + EXPECT_FLOAT_EQ(unsupportedCaps.MinFragmentInterpolationOffset, -0.5f); + EXPECT_FLOAT_EQ(unsupportedCaps.MaxFragmentInterpolationOffset, 0.4375f); + EXPECT_EQ(unsupportedCaps.FragmentInterpolationOffsetBits, 4); + + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation"); + // A stale error from an earlier capability probe must not make the optional + // interpolation query look like it failed. + g_fake.pendingError = GL_INVALID_OPERATION; + MobileGL::MG_External::GLESCapabilities supportedCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs)); + EXPECT_TRUE(supportedCaps.SupportsShaderMultisampleInterpolation); + EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried); + EXPECT_FLOAT_EQ(supportedCaps.MinFragmentInterpolationOffset, g_fake.minFragmentInterpolationOffset); + EXPECT_FLOAT_EQ(supportedCaps.MaxFragmentInterpolationOffset, g_fake.maxFragmentInterpolationOffset); + EXPECT_EQ(supportedCaps.FragmentInterpolationOffsetBits, g_fake.fragmentInterpolationOffsetBits); + EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR); +} + +TEST(FragmentInterpolationCapabilities, QueryErrorIsDrainedAndFallsBackToCoreMinimums) { + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation"); + g_fake.fragmentInterpolationQueryRaisesError = true; + const auto funcs = MakeFakeGLESFunctions(); + + MobileGL::MG_External::GLESCapabilities caps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs)); + + EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried); + EXPECT_FLOAT_EQ(caps.MinFragmentInterpolationOffset, -0.5f); + EXPECT_FLOAT_EQ(caps.MaxFragmentInterpolationOffset, 0.4375f); + EXPECT_EQ(caps.FragmentInterpolationOffsetBits, 4); + EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR); +} + // The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising // it on a driver that cannot filter anisotropically would leave them silently on trilinear. TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) { diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index a0e2ee35..d2a4e668 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -197,13 +197,13 @@ TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) { EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end()); } -TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) { +TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsAtExperimentalCTSVersion) { MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend; const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo; const auto& extensions = rendererInfo.Extensions; - EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3); - EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3); + EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4); + EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 6); EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0); EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader), @@ -397,13 +397,13 @@ TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuf MobileGL::IntVec2(512, 512)); } -TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) { +TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsAtExperimentalCTSVersion) { MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo; const auto& extensions = rendererInfo.Extensions; - EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3); - EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3); + EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4); + EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 6); EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0); EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader), @@ -516,6 +516,50 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) { EXPECT_EQ(params.MaxComputeImageUniforms, 5); } +TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) { + using namespace MobileGL; + + MG_External::GLESCapabilities glesCaps; + glesCaps.MinFragmentInterpolationOffset = -0.75f; + glesCaps.MaxFragmentInterpolationOffset = 0.625f; + glesCaps.FragmentInterpolationOffsetBits = 6; + MG_Backend::DirectGLES::BackendObject_DirectGLES glesBackend; + glesBackend.ApplyGLESCapabilitiesForTesting(glesCaps); + EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.75f); + EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.625f); + EXPECT_EQ(glesBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 6); + + VkPhysicalDeviceProperties properties{}; + // A common Vulkan limit pair: max is one representable 4-bit step below 0.5. + properties.limits.minInterpolationOffset = -0.5f; + properties.limits.maxInterpolationOffset = 0.4375f; + properties.limits.subPixelInterpolationOffsetBits = 4; + MG_External::VulkanCapabilities vkCaps; + MG_Util::BackendLoader::FillInVulkanCapabilities(vkCaps, properties); + EXPECT_FLOAT_EQ(vkCaps.MinFragmentInterpolationOffset, -0.5f); + EXPECT_FLOAT_EQ(vkCaps.MaxFragmentInterpolationOffset, 0.4375f); + EXPECT_EQ(vkCaps.FragmentInterpolationOffsetBits, 4); + + vkCaps.MinFragmentInterpolationOffset = -0.875f; + vkCaps.MaxFragmentInterpolationOffset = 0.75f; + vkCaps.FragmentInterpolationOffsetBits = 7; + MG_Backend::DirectVulkan::BackendObject_DirectVulkan vkBackend; + vkBackend.ApplyVulkanCapabilitiesForTesting(vkCaps); + EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.875f); + EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.75f); + EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 7); + + // Invalid/zero host data cannot under-advertise the OpenGL 4 minimums. + MG_External::VulkanCapabilities invalidCaps; + invalidCaps.MinFragmentInterpolationOffset = 0.0f; + invalidCaps.MaxFragmentInterpolationOffset = 0.0f; + invalidCaps.FragmentInterpolationOffsetBits = 0; + vkBackend.ApplyVulkanCapabilitiesForTesting(invalidCaps); + EXPECT_LE(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.5f); + EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.4375f); + EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 4); +} + TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) { using namespace MobileGL; @@ -638,6 +682,48 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) { MG_State::pGLContext.reset(); } +TEST(GetterSanity, ReportsFragmentInterpolationLimitsForFloatAndIntegerQueries) { + using namespace MobileGL; + + auto previousContext = Move(MG_State::pGLContext); + auto previousBackend = Move(MG_Backend::pActiveBackendObject); + MG_State::pGLContext = MakeUnique(); + + MG_Backend::DynamicBackendParameters params; + params.MinFragmentInterpolationOffset = -0.75f; + params.MaxFragmentInterpolationOffset = 0.4375f; + params.FragmentInterpolationOffsetBits = 6; + MG_Backend::pActiveBackendObject = MakeUnique(params); + + GLfloat floatValue = 0.0f; + MG_Impl::GLImpl::GetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &floatValue); + EXPECT_FLOAT_EQ(floatValue, -0.75f); + MG_Impl::GLImpl::GetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &floatValue); + EXPECT_FLOAT_EQ(floatValue, 0.4375f); + MG_Impl::GLImpl::GetFloatv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &floatValue); + EXPECT_FLOAT_EQ(floatValue, 6.0f); + + GLint intValue = 0; + MG_Impl::GLImpl::GetIntegerv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &intValue); + EXPECT_EQ(intValue, -1); + MG_Impl::GLImpl::GetIntegerv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &intValue); + EXPECT_EQ(intValue, 0); + MG_Impl::GLImpl::GetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &intValue); + EXPECT_EQ(intValue, 6); + + GLboolean boolValue = GL_FALSE; + MG_Impl::GLImpl::GetBooleanv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &boolValue); + EXPECT_EQ(boolValue, GL_TRUE); + MG_Impl::GLImpl::GetBooleanv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &boolValue); + EXPECT_EQ(boolValue, GL_TRUE); + MG_Impl::GLImpl::GetBooleanv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &boolValue); + EXPECT_EQ(boolValue, GL_TRUE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Backend::pActiveBackendObject = Move(previousBackend); + MG_State::pGLContext = Move(previousContext); +} + TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) { using namespace MobileGL; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 83101308..97605f01 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -9,6 +9,7 @@ #include "Loader.h" #include "MG_Util/Types.h" #include +#include #if defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN 1 @@ -838,8 +839,14 @@ namespace MobileGL::MG_Util::BackendLoader { if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) { caps.SupportsNoperspectiveInterpolation = true; } + if (std::strcmp(extension, "GL_OES_shader_multisample_interpolation") == 0) { + caps.SupportsShaderMultisampleInterpolation = true; + } } } + caps.SupportsShaderMultisampleInterpolation = + caps.SupportsShaderMultisampleInterpolation || caps.GLESVersion.Major > 3 || + (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2); // Detect optional raster/color-mask entry points by whether they loaded. glColorMaski is GLES // 3.2 core (no extension string), so pointer presence is the reliable signal for all of these. @@ -900,6 +907,9 @@ namespace MobileGL::MG_Util::BackendLoader { GLint maxColorAttachments = 8; GLint maxClipDistances = 8; GLint maxViewports = 16; + GLfloat minFragmentInterpolationOffset = -0.5f; + GLfloat maxFragmentInterpolationOffset = 0.4375f; + GLint fragmentInterpolationOffsetBits = 4; glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange); glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange); glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity); @@ -950,6 +960,29 @@ namespace MobileGL::MG_Util::BackendLoader { glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports); glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims); glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits); + if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) { + const auto drainErrors = [&glesFuncs]() { + Bool hadError = false; + if (glesFuncs.glGetError) { + while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true; + } + return hadError; + }; + + // Isolate these optional queries from errors raised by preceding capability + // probes, then consume any query error so initialization never leaks it into + // the application's first glGetError call. + drainErrors(); + glesFuncs.glGetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &minFragmentInterpolationOffset); + glesFuncs.glGetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &maxFragmentInterpolationOffset); + glesFuncs.glGetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &fragmentInterpolationOffsetBits); + if (drainErrors()) { + MGLOG_W("Fragment interpolation limit query failed; using OpenGL minimums"); + minFragmentInterpolationOffset = -0.5f; + maxFragmentInterpolationOffset = 0.4375f; + fragmentInterpolationOffsetBits = 4; + } + } // Only legal to query once the extension has been seen in the loop above, hence not batched // with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM. if (caps.SupportsTextureFilterAnisotropy) { @@ -1007,6 +1040,19 @@ namespace MobileGL::MG_Util::BackendLoader { caps.ViewportBoundsRangeMin = viewportBoundsRange[0]; caps.ViewportBoundsRangeMax = viewportBoundsRange[1]; caps.ViewportSubpixelBits = viewportSubpixelBits; + caps.MinFragmentInterpolationOffset = + std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f + ? minFragmentInterpolationOffset + : -0.5f; + caps.MaxFragmentInterpolationOffset = 0.4375f; + caps.FragmentInterpolationOffsetBits = 4; + if (fragmentInterpolationOffsetBits >= 4 && std::isfinite(maxFragmentInterpolationOffset)) { + const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -fragmentInterpolationOffsetBits); + if (maxFragmentInterpolationOffset >= requiredMaxOffset) { + caps.MaxFragmentInterpolationOffset = maxFragmentInterpolationOffset; + caps.FragmentInterpolationOffsetBits = fragmentInterpolationOffsetBits; + } + } MGLOG_I(" GL_ALIASED_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.AliasedLineWidthRangeMin, caps.AliasedLineWidthRangeMax); MGLOG_I(" GL_SMOOTH_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.SmoothLineWidthRangeMin, diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index 12667d45..fa544e40 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1055,6 +1055,9 @@ namespace MobileGL { // SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back // to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass. Bool SupportsNoperspectiveInterpolation = false; + // GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes + // interpolateAtOffset and the three fragment-offset limit queries. + Bool SupportsShaderMultisampleInterpolation = false; // GL_RENDERER contains "ANGLE". Bool IsAngleRenderer = false; // GL_RENDERER contains both "ANGLE" and "llvmpipe". @@ -1120,6 +1123,9 @@ namespace MobileGL { Float ViewportBoundsRangeMin = 0.0f; Float ViewportBoundsRangeMax = 0.0f; Int ViewportSubpixelBits = 0; + Float MinFragmentInterpolationOffset = -0.5f; + Float MaxFragmentInterpolationOffset = 0.4375f; + Int FragmentInterpolationOffsetBits = 4; }; } // namespace MG_External diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp index 2e710e49..be3e90d9 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp @@ -9,6 +9,7 @@ #include "Loader.h" #include +#include namespace MobileGL::MG_Util::BackendLoader { namespace { @@ -40,6 +41,25 @@ namespace MobileGL::MG_Util::BackendLoader { return MaxSampleCountFromFlags(commonFlags); } + void FillFragmentInterpolationLimits(MG_External::VulkanCapabilities& caps, + const VkPhysicalDeviceLimits& limits) { + caps.MinFragmentInterpolationOffset = + std::isfinite(limits.minInterpolationOffset) && limits.minInterpolationOffset <= -0.5f + ? limits.minInterpolationOffset + : -0.5f; + + caps.MaxFragmentInterpolationOffset = 0.4375f; + caps.FragmentInterpolationOffsetBits = 4; + const Int bits = static_cast(limits.subPixelInterpolationOffsetBits); + if (bits >= 4 && std::isfinite(limits.maxInterpolationOffset)) { + const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -bits); + if (limits.maxInterpolationOffset >= requiredMaxOffset) { + caps.MaxFragmentInterpolationOffset = limits.maxInterpolationOffset; + caps.FragmentInterpolationOffsetBits = bits; + } + } + } + VulkanDynamicFunctions LoadVulkanDynamicFunctions(VkInstance instance) { VulkanDynamicFunctions loaded{}; if (instance == VK_NULL_HANDLE) { @@ -171,6 +191,7 @@ namespace MobileGL::MG_Util::BackendLoader { caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0]; caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1]; caps.ViewportSubpixelBits = static_cast(p.limits.viewportSubPixelBits); + FillFragmentInterpolationLimits(caps, p.limits); VkPhysicalDeviceFeatures supportedFeatures{}; vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures); @@ -261,6 +282,7 @@ namespace MobileGL::MG_Util::BackendLoader { caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0]; caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1]; caps.ViewportSubpixelBits = static_cast(properties.limits.viewportSubPixelBits); + FillFragmentInterpolationLimits(caps, properties.limits); caps.SupportsWideLines = false; // This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional // stage writes disabled rather than inferring them from descriptor limits alone. diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h index 6114579e..fea97466 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h @@ -68,6 +68,9 @@ namespace MobileGL { Float ViewportBoundsRangeMin = 0.0f; Float ViewportBoundsRangeMax = 0.0f; Int ViewportSubpixelBits = 0; + Float MinFragmentInterpolationOffset = -0.5f; + Float MaxFragmentInterpolationOffset = 0.4375f; + Int FragmentInterpolationOffsetBits = 4; Bool SupportsWideLines = false; // Storage-image descriptors are limited per stage by // maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally