diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 07cb2a66..7adec448 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -2309,6 +2309,29 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + if (tailSpanDirty) { // Sample shading (ARB_sample_shading; ES 3.2 core) + // Both halves are gated on the same entry point rather than on a version check: + // GL_SAMPLE_SHADING and glMinSampleShading arrived together (ES 3.2 core / + // OES_sample_shading), so a null pointer means glEnable(GL_SAMPLE_SHADING) would + // only push an INVALID_ENUM into the driver's queue. This is NOT part of the + // SYNC_CAPABILITY block above for exactly that reason - that macro has nowhere to + // put a guard. + if (g_GLESFuncs.glMinSampleShading) { + if (forceFullPush || + parameters.SampleShadingEnabled != g_syncedRenderStateParameters.SampleShadingEnabled) { + if (parameters.SampleShadingEnabled) { + g_GLESFuncs.glEnable(GL_SAMPLE_SHADING); + } else { + g_GLESFuncs.glDisable(GL_SAMPLE_SHADING); + } + } + if (forceFullPush || parameters.MinSampleShadingValue != + g_syncedRenderStateParameters.MinSampleShadingValue) { + g_GLESFuncs.glMinSampleShading(parameters.MinSampleShadingValue); + } + } + } + g_syncedRenderStateVersion = currentRenderStateVersion; // Byte copy, not member copy: it also clones the frontend struct's padding bytes, // which is what lets the span memcmps above answer "unchanged" exactly instead of diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index c4b1d230..de34cfba 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -201,6 +201,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable))); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); XXHASH_VERIFY( @@ -437,6 +439,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; ms.rasterizationSamples = payload.rasterizationSamples; + ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE; + // Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the + // struct's bytes match the hash the payload was keyed by. + ms.minSampleShading = payload.minSampleShading; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index a0f7f0c5..f808395a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -37,6 +37,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkRenderPass renderPass = VK_NULL_HANDLE; Uint32 colorAttachmentCount = 1; VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + // glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the + // pipeline rather than exposing as dynamic state - so both are part of the pipeline's + // identity and both are hashed. The renderer leaves the enable false unless the + // device's sampleRateShading feature was enabled + // (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784). + Bool sampleShadingEnable = false; + Float minSampleShading = 0.0f; Uint32 subpass = 0; VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; Bool primitiveRestartEnable = false; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index c82a1d25..1b6b5160 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -5133,6 +5133,13 @@ void main() { .renderPass = renderPassEntry.renderPass, .colorAttachmentCount = renderPassEntry.colorAttachmentCount, .rasterizationSamples = renderPassEntry.sampleCount, + // ARB_sample_shading. Dropped on a device without sampleRateShading rather than + // hard-failing the draw: the rate is a hint, and the pipeline renders correctly at the + // driver's own rate. Both halves move the render state's PIPELINE version, so a cached + // pipeline built at the old rate cannot be handed back for the new one. + .sampleShadingEnable = m_sampleRateShadingFeatureEnabled && + MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading), + .minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(), .subpass = 0, .topology = vkTopology, .primitiveRestartEnable = primitiveRestartEnabled, @@ -12809,6 +12816,11 @@ void main() { m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE; deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend; m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE; + // ARB_sample_shading. Without this feature a pipeline may not set sampleShadingEnable + // (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784), so the GL enable + // has to be dropped rather than forwarded - which is what the flag below records. + deviceFeatures.sampleRateShading = supportedDeviceFeatures.sampleRateShading; + m_sampleRateShadingFeatureEnabled = deviceFeatures.sampleRateShading == VK_TRUE; // ARB_viewport_array rasterization. Without multiViewport a pipeline may declare exactly // one viewport (VUID-VkPipelineViewportStateCreateInfo-viewportCount-01216), so a shader's // gl_ViewportIndex can only ever select viewport 0 and the other fifteen rectangles are diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 850e8dba..67d5943d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -584,6 +584,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { // needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent. Bool m_dualSrcBlendFeatureEnabled = false; Bool m_primitiveTopologyListRestartFeatureEnabled = false; + // sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e. + // glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT + // hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate + // pipeline is still correct (just not per-sample) at the default rate - so the enable is + // dropped and the draw proceeds, which is what a GL implementation with SAMPLES=1 does too. + Bool m_sampleRateShadingFeatureEnabled = false; // multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports // (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1 // when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 94bc5aab..9de67de0 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -411,7 +411,7 @@ DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLs DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params) -DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value) +DECLARE_GL_FUNCTION_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MinSampleShading, value) DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value) DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params) DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 096c26a3..fee468e0 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -682,7 +682,10 @@ namespace MobileGL::MG_Impl::GLImpl { return; case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET: case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET: - case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: { + case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: + // Same reason as the three above: the integer fallback would round the fraction to 0 + // or 1 first, so a 0.25 sample-shading rate would answer GL_FALSE. + case GL_MIN_SAMPLE_SHADING_VALUE: { GLfloat value = 0.0f; GetFloatv(pname, &value); *params = value != 0.0f ? GL_TRUE : GL_FALSE; @@ -848,6 +851,11 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_SAMPLE_COVERAGE_VALUE: params[0] = MG_State::pGLContext->GetSampleCoverageValue(); return; + case GL_MIN_SAMPLE_SHADING_VALUE: + // Float state, so it has to be answered here rather than through the integer + // fallback: glMinSampleShading(0.5) must read back as 0.5 and not as 0. + params[0] = MG_State::pGLContext->GetMinSampleShadingValue(); + return; case GL_POINT_FADE_THRESHOLD_SIZE: // Float state: read it directly so the fractional part is not lost to the integer path. params[0] = MG_State::pGLContext->GetPointFadeThresholdSize(); @@ -1941,6 +1949,13 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_SAMPLE_MASK: *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) ? GL_TRUE : GL_FALSE; return; + case GL_SAMPLE_SHADING: + *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading) ? GL_TRUE : GL_FALSE; + return; + case GL_MIN_SAMPLE_SHADING_VALUE: + // GL 4.6 core 22.2: a floating-point value queried as an integer rounds to nearest. + *params = static_cast(std::lround(MG_State::pGLContext->GetMinSampleShadingValue())); + return; case GL_SAMPLE_MASK_VALUE: *params = static_cast(MG_State::pGLContext->GetSampleMaskValue()); return; diff --git a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp index 6ccf39bf..74af2c85 100644 --- a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp +++ b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp @@ -328,6 +328,14 @@ namespace MobileGL::MG_Impl::GLImpl { MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast(value), 0.0f, 1.0f), invert == GL_TRUE); } + // ARB_sample_shading / GL 4.6 core 14.3.1: "value is clamped to [0, 1] when specified", so + // there is no error to raise - a caller that asks for 2.0 gets 1.0 and GL_MIN_SAMPLE_SHADING_- + // VALUE reads back 1.0. Was a logging no-op while ARB_sample_shading was advertised, which + // let an application enable GL_SAMPLE_SHADING and then quietly get the driver's default rate. + void MinSampleShading_State(GLfloat value) { + MG_State::pGLContext->SetMinSampleShadingValue(std::clamp(static_cast(value), 0.0f, 1.0f)); + } + void PolygonOffset_State(GLfloat factor, GLfloat units) { MG_State::pGLContext->SetPolygonOffset(static_cast(factor), static_cast(units)); } @@ -1013,6 +1021,10 @@ namespace MobileGL::MG_Impl::GLImpl { SampleCoverage_State(value, invert); } + void MinSampleShading(GLfloat value) { + MinSampleShading_State(value); + } + void PolygonOffset(GLfloat factor, GLfloat units) { PolygonOffset_State(factor, units); } diff --git a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.h b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.h index 30b668eb..473be0ee 100644 --- a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.h +++ b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.h @@ -38,6 +38,7 @@ namespace MobileGL::MG_Impl::GLImpl { void StencilFunc(GLenum func, GLint ref, GLuint mask); void Scissor(GLint x, GLint y, GLsizei width, GLsizei height); void SampleCoverage(GLfloat value, GLboolean invert); + void MinSampleShading(GLfloat value); void PolygonOffset(GLfloat factor, GLfloat units); void PolygonMode(GLenum face, GLenum mode); void PointSize(GLfloat size); diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 44ff3579..f3a5ac45 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -1025,6 +1025,14 @@ namespace MobileGL::MG_State { return m_renderState.GetSampleMaskValue(); } + void GLContext::SetMinSampleShadingValue(Float value) { + m_renderState.SetMinSampleShadingValue(value); + } + + Float GLContext::GetMinSampleShadingValue() const { + return m_renderState.GetMinSampleShadingValue(); + } + void GLContext::SetPixelStoreParam(PixelStoreParam param, Int value) { m_renderState.SetPixelStoreParam(param, value); } diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 4b32f5ab..425de586 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -280,6 +280,8 @@ namespace MobileGL { Bool GetSampleCoverageInvert() const; void SetSampleMaskValue(Uint32 mask); Uint32 GetSampleMaskValue() const; + void SetMinSampleShadingValue(Float value); + Float GetMinSampleShadingValue() const; void SetPixelStoreParam(PixelStoreParam param, Int value); Int GetPixelStoreParam(PixelStoreParam param) const; PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const; diff --git a/MobileGL/MG_State/GLState/RenderState/RenderState.cpp b/MobileGL/MG_State/GLState/RenderState/RenderState.cpp index cd96d084..c2aef284 100644 --- a/MobileGL/MG_State/GLState/RenderState/RenderState.cpp +++ b/MobileGL/MG_State/GLState/RenderState/RenderState.cpp @@ -300,6 +300,7 @@ namespace MobileGL { SET_CAPABILITY(SampleAlphaToOne, enabled); SET_CAPABILITY(SampleCoverage, enabled); SET_CAPABILITY(SampleMask, enabled); + SET_CAPABILITY(SampleShading, enabled); SET_CAPABILITY(StencilTest, enabled); SET_CAPABILITY(ProgramPointSize, enabled); case CapabilityInput::Blend: { @@ -374,6 +375,7 @@ namespace MobileGL { RETURN_CAPABILITY(SampleAlphaToOne); RETURN_CAPABILITY(SampleCoverage); RETURN_CAPABILITY(SampleMask); + RETURN_CAPABILITY(SampleShading); RETURN_CAPABILITY(StencilTest); RETURN_CAPABILITY(ProgramPointSize); case CapabilityInput::Blend: @@ -767,6 +769,20 @@ namespace MobileGL { return m_parameters.SampleMaskValue; } + void RenderState::SetMinSampleShadingValue(Float value) { + if (m_parameters.MinSampleShadingValue == value) return; + + m_parameters.MinSampleShadingValue = value; + // BumpVersions, not just ++m_version: DirectVulkan bakes the fraction into + // VkPipelineMultisampleStateCreateInfo::minSampleShading, so a cached pipeline + // built with the old value must not be reused. + BumpVersions(); + } + + Float RenderState::GetMinSampleShadingValue() const { + return m_parameters.MinSampleShadingValue; + } + // -------------------- Pixel Store -------------------- void RenderState::SetPixelStoreParam(PixelStoreParam param, Int value) { #define SET_PIXEL_STORE_PARAM(paramNameHead, paramNameTail, val) \ diff --git a/MobileGL/MG_State/GLState/RenderState/RenderState.h b/MobileGL/MG_State/GLState/RenderState/RenderState.h index 589d0644..254eb884 100644 --- a/MobileGL/MG_State/GLState/RenderState/RenderState.h +++ b/MobileGL/MG_State/GLState/RenderState/RenderState.h @@ -278,6 +278,10 @@ namespace MobileGL { Float SampleCoverageValue = 1.0f; Bool SampleCoverageInvert = false; Uint32 SampleMaskValue = 0xffffffffu; + // glMinSampleShading (ARB_sample_shading / GL 4.0 core 14.3.1). The fraction of samples + // that get their own independent shading when GL_SAMPLE_SHADING is enabled; the initial + // value is 0, and the value is clamped to [0, 1] on the way in. + Float MinSampleShadingValue = 0.0f; Array StencilStates{}; // Cull Face @@ -326,6 +330,7 @@ namespace MobileGL { Bool SampleAlphaToOneEnabled = false; Bool SampleCoverageEnabled = false; Bool SampleMaskEnabled = false; + Bool SampleShadingEnabled = false; Bool StencilTestEnabled = false; Bool ProgramPointSizeEnabled = false; // glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one @@ -465,6 +470,9 @@ namespace MobileGL { Bool GetSampleCoverageInvert() const; void SetSampleMaskValue(Uint32 mask); Uint32 GetSampleMaskValue() const; + // glMinSampleShading. `value` is stored as given; the entry point clamps. + void SetMinSampleShadingValue(Float value); + Float GetMinSampleShadingValue() const; // Pixel Store void SetPixelStoreParam(PixelStoreParam param, Int value); diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index 1fa25eee..1fcf696e 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -3891,3 +3891,85 @@ void main() { DrainProgramTestErrors(); } + +// gl_NumSamples has no SPIR-V built-in, so the source pipeline lowers it onto a reserved +// default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME). Two things have to hold at +// once: the program has to BUILD (it used to die at compile with "'gl_NumSamples' : undeclared +// identifier", which is what took all 144 KHR-GL46.sample_variables.mask.* bodies down), and the +// uniform standing in for the built-in has to stay invisible to GL - gl_NumSamples is a built-in, +// so a conformant implementation reports nothing for it and no glUniform* may reach it. +TEST_F(ProgramTest, GlNumSamplesLowersToAHiddenReservedUniform) { + const char* vsSource = R"(#version 460 core +void main() { gl_Position = vec4(0.0); } +)"; + const char* fsSource = R"(#version 460 core +uniform int u_sampleMask; +layout(location = 0) out vec4 o_color; +void main() { + for (int i = 0; i < (gl_NumSamples + 31) / 32; ++i) { + gl_SampleMask[i] = u_sampleMask & gl_SampleMaskIn[i]; + } + o_color = vec4(1.0, 0.0, 0.0, 1.0); +} +)"; + GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource); + GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource); + GLuint program = LinkVsFs(vs, fs, GL_TRUE); + + // u_sampleMask and nothing else: the stand-in must not enlarge the enumeration. + GLint activeUniforms = 0; + GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeUniforms); + EXPECT_EQ(activeUniforms, 1); + EXPECT_NE(GetUniformLocation(program, "u_sampleMask"), -1); + EXPECT_EQ(GetUniformLocation(program, "mg_NumSamples"), -1); + EXPECT_EQ(GetUniformLocation(program, "gl_NumSamples"), -1); + EXPECT_EQ(GetUniformBlockIndex(program, "MGL_GLOBAL_UBO"), GL_INVALID_INDEX); + + char nameBuf[64] = ""; + for (GLint i = 0; i < activeUniforms; ++i) { + GLsizei nameLen = 0; + GLint size = 0; + GLenum type = 0; + GetActiveUniform(program, static_cast(i), sizeof(nameBuf), &nameLen, &size, &type, nameBuf); + EXPECT_TRUE(std::strcmp(nameBuf, "mg_NumSamples") != 0) << nameBuf; + } + + // The driver-side write path, which is what the draw path calls. It reports true only when the + // program really did take the shim AND the optimized SPIR-V kept the member. + const auto& programObject = MG_State::pGLContext->GetProgramObject(program); + ASSERT_NE(programObject, nullptr); + EXPECT_TRUE(programObject->UsesReservedNumSamples()); + EXPECT_TRUE(programObject->WriteReservedNumSamples(4)); + + const Uint32 versionAfterFirstWrite = programObject->GetUBOContentVersion(); + // Value-identical rewrite: no re-upload, so no version bump - a run of draws into one + // framebuffer must not dirty the UBO every draw. + EXPECT_TRUE(programObject->WriteReservedNumSamples(4)); + EXPECT_EQ(programObject->GetUBOContentVersion(), versionAfterFirstWrite); + // A different framebuffer's sample count does have to reach the GPU. + EXPECT_TRUE(programObject->WriteReservedNumSamples(1)); + EXPECT_NE(programObject->GetUBOContentVersion(), versionAfterFirstWrite); + + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +// A program whose fragment stage never mentions gl_NumSamples pays nothing and has nothing to +// write - the gate the draw path reads before it touches the SPIR-V join. +TEST_F(ProgramTest, ProgramWithoutGlNumSamplesHasNoReservedUniform) { + const char* vsSource = R"(#version 460 core +void main() { gl_Position = vec4(0.0); } +)"; + const char* fsSource = R"(#version 460 core +layout(location = 0) out vec4 o_color; +void main() { o_color = vec4(1.0); } +)"; + GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource); + GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource); + GLuint program = LinkVsFs(vs, fs, GL_TRUE); + + const auto& programObject = MG_State::pGLContext->GetProgramObject(program); + ASSERT_NE(programObject, nullptr); + EXPECT_FALSE(programObject->UsesReservedNumSamples()); + EXPECT_FALSE(programObject->WriteReservedNumSamples(4)); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index 33dbc414..a7b8f1ae 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -4569,3 +4570,275 @@ subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); } << "an inactive #if arm must not have an unconditional forwarding body appended for it"; } + +// --------------------------------------------------------------------------------------------- +// gl_NumSamples: glslang declares the built-in only when it is NOT targeting SPIR-V, and MobileGL +// always targets SPIR-V, so every fragment shader that reads it used to die at compile time with +// "'gl_NumSamples' : undeclared identifier". InjectNumSamplesBuiltinShim lowers it onto a reserved +// default-block uniform instead; the draw path fills that uniform in. +// --------------------------------------------------------------------------------------------- + +namespace { + Bool HasNumSamplesShim(const String& source) { + return source.find("uniform int mg_NumSamples;") != String::npos && + source.find("#define gl_NumSamples mg_NumSamples") != String::npos; + } + + void ExpectShaderCompiles(GLenum stage, const String& source) { + using namespace MG_Util::ShaderTranspiler; + ShaderAttrib attrib{.shaderType = stage, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } + } +} // namespace + +TEST_F(ProgramUtilTest, PreprocessFragmentShaderInjectsNumSamplesShim) { + using namespace MG_Util::ShaderTranspiler; + + // The shape KHR-GL46.sample_variables.mask.* uses: gl_NumSamples as the bound of the loop that + // writes gl_SampleMask. + String source = R"(#version 460 core +layout(location = 0) out highp vec4 o_color; +uniform int u_sampleMask; +void main() { + for (int i = 0; i < (gl_NumSamples + 31) / 32; ++i) { + gl_SampleMask[i] = u_sampleMask & gl_SampleMaskIn[i]; + } + o_color = vec4(1, 0, 0, 1); +} +)"; + + PreprocessShaderSource(ShaderStage::Fragment, source); + EXPECT_TRUE(HasNumSamplesShim(source)) << source; + ExpectShaderCompiles(GL_FRAGMENT_SHADER, source); +} + +TEST_F(ProgramUtilTest, NumSamplesShimIgnoresCommentedAndPartialTokens) { + using namespace MG_Util::ShaderTranspiler; + + // Comment and string text is masked before the token scan, and the scan is whole-identifier: + // "gl_NumSamplesFoo" is a different name and must not drag the shim in. + String commented = R"(#version 460 core +out vec4 fragColor; +// gl_NumSamples used to be read here +/* gl_NumSamples */ +void main() { fragColor = vec4(1.0); } +)"; + String suffixed = R"(#version 460 core +out vec4 fragColor; +uniform int gl_NumSamplesFoo; +void main() { fragColor = vec4(float(gl_NumSamplesFoo)); } +)"; + + for (String* source : {&commented, &suffixed}) { + PreprocessShaderSource(ShaderStage::Fragment, *source); + EXPECT_EQ(source->find("mg_NumSamples"), String::npos) << *source; + } +} + +TEST_F(ProgramUtilTest, NumSamplesShimDoesNotDoubleInject) { + using namespace MG_Util::ShaderTranspiler; + + // Re-running the preprocessor over its own output must be a no-op for this pass; a second + // "uniform int mg_NumSamples;" would not compile. + String source = R"(#version 460 core +out vec4 fragColor; +void main() { fragColor = vec4(float(gl_NumSamples)); } +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + ASSERT_TRUE(HasNumSamplesShim(source)) << source; + + const String once = source; + PreprocessShaderSource(ShaderStage::Fragment, source); + EXPECT_EQ(source, once) << "the shim re-fired on an already-shimmed source"; + + // Same guard for an application that happens to own the name itself. + String applicationOwned = R"(#version 460 core +uniform int mg_NumSamples; +out vec4 fragColor; +void main() { fragColor = vec4(float(gl_NumSamples + mg_NumSamples)); } +)"; + const String before = applicationOwned; + PreprocessShaderSource(ShaderStage::Fragment, applicationOwned); + EXPECT_EQ(applicationOwned, before); +} + +TEST_F(ProgramUtilTest, NumSamplesShimIsFragmentStageOnly) { + using namespace MG_Util::ShaderTranspiler; + + // gl_NumSamples exists in the fragment stage and nowhere else, so a vertex or geometry source + // naming it must be left for glslang to reject rather than quietly legalized. + for (const ShaderStage stage : {ShaderStage::Vertex, ShaderStage::Geometry, ShaderStage::Compute}) { + String source = R"(#version 460 core +out int v; +void main() { v = gl_NumSamples; } +)"; + PreprocessShaderSource(stage, source); + EXPECT_EQ(source.find("mg_NumSamples"), String::npos) << static_cast(stage) << ":\n" << source; + } +} + +TEST_F(ProgramUtilTest, NumSamplesShimHonoursTheVersionAndExtensionGate) { + using namespace MG_Util::ShaderTranspiler; + + struct Case { + const char* label; + const char* versionBlock; + Bool expectShim; + }; + // Mirrors glslang's own gate (Initialize.cpp): desktop from 4.00, or from 1.30 with + // ARB_sample_shading; ESSL from 3.20, or from 3.10 with OES_sample_variables. + const Case cases[] = { + {"desktop 460 core", "#version 460 core\n", true}, + {"desktop 400 core", "#version 400 core\n", true}, + {"desktop 330 core, no extension", "#version 330 core\n", false}, + {"desktop 330 core + ARB_sample_shading", + "#version 330 core\n#extension GL_ARB_sample_shading : require\n", true}, + {"desktop 120, no extension", "#version 120\n", false}, + {"ESSL 320", "#version 320 es\n", true}, + {"ESSL 310, no extension", "#version 310 es\n", false}, + {"ESSL 310 + OES_sample_variables", + "#version 310 es\n#extension GL_OES_sample_variables : require\n", true}, + {"ESSL 300", "#version 300 es\n", false}, + }; + + for (const Case& testCase : cases) { + SCOPED_TRACE(testCase.label); + String source = String(testCase.versionBlock) + R"(out vec4 fragColor; +void main() { fragColor = vec4(float(gl_NumSamples)); } +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + EXPECT_EQ(HasNumSamplesShim(source), testCase.expectShim) << source; + } +} + +// --------------------------------------------------------------------------------------------- +// ES preamble extension macros. Rewriting "#version 310 es" to "#version 460 core" makes glslang +// emit its DESKTOP preamble, which defines none of the OES/AEP extension macros - so a shader's +// own "#if !GL_OES_sample_variables" guard takes the branch it was written to avoid. +// --------------------------------------------------------------------------------------------- + +TEST_F(ProgramUtilTest, EsSourceRegainsThePreambleMacrosForTheExtensionsItNames) { + using namespace MG_Util::ShaderTranspiler; + + // KHR-GL46.es_31_compatibility.sample_variables.verification.extension in miniature: the + // deliberately-broken arm must stay unreached. + String source = R"(#version 310 es +#extension GL_OES_sample_variables : enable +precision highp float; +out vec4 fragColor; +#if !GL_OES_sample_variables +this is broken +#endif +void main() { fragColor = vec4(1.0); } +)"; + + PreprocessShaderSource(ShaderStage::Fragment, source); + EXPECT_NE(source.find("#define GL_OES_sample_variables 1"), String::npos) << source; + ExpectShaderCompiles(GL_FRAGMENT_SHADER, source); +} + +TEST_F(ProgramUtilTest, EsPreambleMacroInjectionStaysNarrow) { + using namespace MG_Util::ShaderTranspiler; + + { + SCOPED_TRACE("an extension the source never names is not defined"); + String source = R"(#version 310 es +#extension GL_OES_sample_variables : enable +out vec4 fragColor; +void main() { fragColor = vec4(1.0); } +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + EXPECT_EQ(source.find("#define GL_OES_shader_image_atomic"), String::npos) << source; + // GL_ES stays undefined on purpose: the shader really is compiled as desktop now, and + // flipping "#ifdef GL_ES" branches would break far more than it fixes. + EXPECT_EQ(source.find("#define GL_ES"), String::npos) << source; + } + + { + SCOPED_TRACE("an extension glslang's DESKTOP preamble already defines is not re-defined"); + String source = R"(#version 310 es +#extension GL_EXT_shader_non_constant_global_initializers : enable +out vec4 fragColor; +void main() { fragColor = vec4(1.0); } +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + EXPECT_EQ(source.find("#define GL_EXT_shader_non_constant_global_initializers"), String::npos) << source; + } + + { + SCOPED_TRACE("a desktop source is untouched - it keeps the preamble it is entitled to"); + String source = R"(#version 460 core +#extension GL_OES_sample_variables : enable +out vec4 fragColor; +void main() { fragColor = vec4(1.0); } +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + EXPECT_EQ(source.find("#define GL_OES_sample_variables"), String::npos) << source; + } +} + +// --------------------------------------------------------------------------------------------- +// A repeated #version directive. glShaderSource concatenates its strings with nothing added +// between them (GL 4.6 core 7.1), so a caller that heads BOTH strings with a #version splices the +// second into the tail of the first - which is what VK-GL-CTS's ShaderImageLoadStoreBase:: +// BuildProgram does. +// --------------------------------------------------------------------------------------------- + +TEST_F(ProgramUtilTest, RepeatedIdenticalVersionDirectiveIsElided) { + using namespace MG_Util::ShaderTranspiler; + + // Byte-for-byte the concatenation the CTS produces: kGLSLPrec ends without a newline, so the + // subcase's own "#version 310 es" lands mid-line. + String source = + "#version 310 es\n\nprecision highp float;\nprecision highp uimage2DArray;#version 310 es\n" + "layout(location = 0) in vec4 i_position;\n" + "void main() { gl_Position = i_position; }\n"; + + const SizeT lineCountBefore = static_cast(std::count(source.begin(), source.end(), '\n')); + PreprocessShaderSource(ShaderStage::Vertex, source); + + // Exactly one #version survives, and the line count is untouched so __LINE__ and every + // glslang diagnostic still point where the application wrote them. + EXPECT_EQ(source.find("#version", source.find("#version") + 1), String::npos) << source; + EXPECT_EQ(static_cast(std::count(source.begin(), source.end(), '\n')), lineCountBefore) << source; + ExpectShaderCompiles(GL_VERTEX_SHADER, source); +} + +TEST_F(ProgramUtilTest, OnlyAnExactVersionRepeatIsElided) { + using namespace MG_Util::ShaderTranspiler; + + { + SCOPED_TRACE("a DIFFERENT second version is left for glslang to reject"); + String source = + "#version 310 es\nprecision highp float;\n#version 320 es\nout vec4 c;\nvoid main() { c = vec4(1.0); }\n"; + PreprocessShaderSource(ShaderStage::Fragment, source); + EXPECT_NE(source.find("#version 320 es"), String::npos) << source; + } + + { + SCOPED_TRACE("a lone non-first #version is still a lone non-first #version"); + // KHR-GL33.shaders.preprocessor.directive.version_not_first_statement_1 requires this to + // fail to compile, and it only does so because the directive is left where it was. + String source = + "precision mediump float;\n#version 330\nout vec4 c;\nvoid main() { c = vec4(1.0); }\n"; + PreprocessShaderSource(ShaderStage::Fragment, source); + const SizeT versionPos = source.find("#version"); + ASSERT_NE(versionPos, String::npos) << source; + EXPECT_NE(versionPos, SizeT{0}) << "the directive must not have been moved to the front:\n" << source; + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + EXPECT_FALSE(res.has_value()) << "a #version preceded by real tokens must still be rejected:\n" << source; + } + + { + SCOPED_TRACE("a MALFORMED repeat is left alone"); + String source = + "#version 330 core\nout vec4 c;\n#version 330 foobar\nvoid main() { c = vec4(1.0); }\n"; + PreprocessShaderSource(ShaderStage::Fragment, source); + EXPECT_NE(source.find("#version 330 foobar"), String::npos) << source; + } +} diff --git a/MobileGL/MG_Test/State/RenderStateTest.cpp b/MobileGL/MG_Test/State/RenderStateTest.cpp index 9523b515..b64b888e 100644 --- a/MobileGL/MG_Test/State/RenderStateTest.cpp +++ b/MobileGL/MG_Test/State/RenderStateTest.cpp @@ -825,3 +825,63 @@ TEST_F(RenderStateTest, PrimitiveRestartCapAndIndexAreBothQueryable) { MG_Impl::GLImpl::PrimitiveRestartIndex(0u); DrainPendingGlErrors(); } + +// glMinSampleShading was a logging no-op while ARB_sample_shading was advertised and +// glEnable(GL_SAMPLE_SHADING) fell through RenderState::SetCapability's default arm, so an +// application could turn sample shading on, ask for a rate, and get neither - with every query +// agreeing that nothing had happened. +TEST_F(RenderStateTest, MinSampleShadingRoundTripsAndClamps) { + DrainPendingGlErrors(); + + // GL 4.6 core table 23.10: the initial value is 0. + GLfloat initial = -1.0f; + MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &initial); + EXPECT_FLOAT_EQ(initial, 0.0f); + + MG_Impl::GLImpl::MinSampleShading(0.25f); + GLfloat value = -1.0f; + MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value); + EXPECT_FLOAT_EQ(value, 0.25f); + + // The fraction survives the double query too, and rounds - not truncates - for the integer one. + GLdouble asDouble = -1.0; + MG_Impl::GLImpl::GetDoublev(GL_MIN_SAMPLE_SHADING_VALUE, &asDouble); + EXPECT_NEAR(asDouble, 0.25, 1e-6); + GLint asInt = -1; + MG_Impl::GLImpl::GetIntegerv(GL_MIN_SAMPLE_SHADING_VALUE, &asInt); + EXPECT_EQ(asInt, 0); + // A non-zero fraction is GL_TRUE, which the integer path would have rounded away first. + GLboolean asBoolean = GL_FALSE; + MG_Impl::GLImpl::GetBooleanv(GL_MIN_SAMPLE_SHADING_VALUE, &asBoolean); + EXPECT_EQ(asBoolean, GL_TRUE); + + // "value is clamped to [0, 1]" - not an error, a clamp. + MG_Impl::GLImpl::MinSampleShading(2.0f); + MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value); + EXPECT_FLOAT_EQ(value, 1.0f); + MG_Impl::GLImpl::MinSampleShading(-3.0f); + MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value); + EXPECT_FLOAT_EQ(value, 0.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::MinSampleShading(0.0f); +} + +TEST_F(RenderStateTest, SampleShadingEnableIsStoredAndQueryable) { + DrainPendingGlErrors(); + + EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_FALSE); + + MG_Impl::GLImpl::Enable(GL_SAMPLE_SHADING); + EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_TRUE); + GLboolean asBoolean = GL_FALSE; + MG_Impl::GLImpl::GetBooleanv(GL_SAMPLE_SHADING, &asBoolean); + EXPECT_EQ(asBoolean, GL_TRUE); + GLint asInt = 0; + MG_Impl::GLImpl::GetIntegerv(GL_SAMPLE_SHADING, &asInt); + EXPECT_EQ(asInt, GL_TRUE); + + MG_Impl::GLImpl::Disable(GL_SAMPLE_SHADING); + EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_FALSE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +}