[Feat] (RenderState): implement glMinSampleShading and the GL_SAMPLE_SHADING enable on both backends

This commit is contained in:
2026-08-27 03:35:55 -04:00
parent 07a0408a28
commit acf86d1fb6
16 changed files with 533 additions and 2 deletions
@@ -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
@@ -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;
@@ -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;
@@ -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
@@ -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
@@ -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)
+16 -1
View File
@@ -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<GLint>(std::lround(MG_State::pGLContext->GetMinSampleShadingValue()));
return;
case GL_SAMPLE_MASK_VALUE:
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
return;
@@ -328,6 +328,14 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(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<Float>(value), 0.0f, 1.0f));
}
void PolygonOffset_State(GLfloat factor, GLfloat units) {
MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(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);
}
@@ -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);
+8
View File
@@ -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);
}
+2
View File
@@ -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;
@@ -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) \
@@ -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<StencilFaceState, 2> 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);
+82
View File
@@ -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<GLuint>(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);
}
@@ -8,6 +8,7 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cstring>
#include <map>
#include <set>
@@ -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<int>(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<SizeT>(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<SizeT>(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;
}
}
@@ -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);
}