mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Feat] (RenderState): implement glMinSampleShading and the GL_SAMPLE_SHADING enable on both backends
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user