mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
[Fix] (Getter): answer the GL 4.6 limit surface honestly - tess/cull/subroutine pnames, TBuiltInResource drift, 84 UBO binding points, 64-bit GL_MAX_ELEMENT_INDEX, per-category sample truth
This commit is contained in:
@@ -41,8 +41,15 @@
|
||||
namespace {
|
||||
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
|
||||
public:
|
||||
explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params):
|
||||
m_params(params) {}
|
||||
// `type` defaults to Unknown, which is what every existing case wanted: a limits-only
|
||||
// double with no backend identity. A case that captures a CompileEnv from it and then
|
||||
// compares the result against glGetIntegerv has to pass a REAL type, because
|
||||
// CompileEnv::HasBackend() is what BuildTBuiltInResource bounds gl_MaxVertexAttribs by
|
||||
// while the getter bounds it by "a backend object exists" - two spellings of the same
|
||||
// thing in production, and only in production.
|
||||
explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params,
|
||||
MobileGL::BackendType type = MobileGL::BackendType::Unknown):
|
||||
m_params(params), m_type(type) {}
|
||||
|
||||
void Initialize() override {}
|
||||
MobileGL::Bool InitCapabilities() override { return true; }
|
||||
@@ -55,10 +62,11 @@ namespace {
|
||||
const MobileGL::MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override {
|
||||
return m_params;
|
||||
}
|
||||
MobileGL::BackendType GetBackendType() const override { return MobileGL::BackendType::Unknown; }
|
||||
MobileGL::BackendType GetBackendType() const override { return m_type; }
|
||||
|
||||
private:
|
||||
MobileGL::MG_Backend::DynamicBackendParameters m_params;
|
||||
MobileGL::BackendType m_type = MobileGL::BackendType::Unknown;
|
||||
MobileGL::MG_Backend::GlobalBackendFunctionsTable m_functions{};
|
||||
MobileGL::RendererInfo m_info{
|
||||
.RendererName = "Test",
|
||||
@@ -722,6 +730,62 @@ TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) {
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
|
||||
}
|
||||
|
||||
// The cull half of the same contract. shaderCullDistance is a SEPARATE feature from
|
||||
// shaderClipDistance - VulkanRenderer enables each independently - so it gets its own gate, and
|
||||
// the combined limit is gated on either being present because GL 4.6 core 11.1.3.10 makes it at
|
||||
// least as large as both halves. These three used to be literal 8s inside BuildTBuiltInResource
|
||||
// with no device consulted at all, which let glslang accept a gl_CullDistance write that then
|
||||
// discarded every primitive it touched.
|
||||
TEST(DirectVulkanSanity, GatesCullDistancesOnTheShaderCullDistanceFeature) {
|
||||
using namespace MobileGL;
|
||||
|
||||
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
|
||||
MG_External::VulkanCapabilities caps;
|
||||
caps.MaxClipDistances = 8;
|
||||
caps.MaxCullDistances = 8;
|
||||
caps.MaxCombinedClipAndCullDistances = 8;
|
||||
|
||||
caps.SupportsShaderClipDistance = false;
|
||||
caps.SupportsShaderCullDistance = false;
|
||||
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0);
|
||||
|
||||
// Clip only: cull stays zero, and the combined limit still describes the clip capacity.
|
||||
caps.SupportsShaderClipDistance = true;
|
||||
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8);
|
||||
|
||||
caps.SupportsShaderCullDistance = true;
|
||||
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8);
|
||||
}
|
||||
|
||||
// DirectGLES reaches clip AND cull distances only through GL_EXT_clip_cull_distance, so the
|
||||
// loader leaves all three at zero without it and the backend forwards that verbatim. Zero is the
|
||||
// answer that stops a gl_CullDistance shader from reaching an ESSL compiler that would reject it.
|
||||
TEST(DirectGLESSanity, ForwardsTheProbedClipAndCullDistanceLimits) {
|
||||
using namespace MobileGL;
|
||||
|
||||
MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
||||
MG_External::GLESCapabilities caps;
|
||||
backend.ApplyGLESCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 0);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0);
|
||||
|
||||
caps.SupportsClipDistance = true;
|
||||
caps.MaxClipDistances = 8;
|
||||
caps.MaxCullDistances = 8;
|
||||
caps.MaxCombinedClipAndCullDistances = 8;
|
||||
backend.ApplyGLESCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8);
|
||||
}
|
||||
|
||||
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX were a hard-coded
|
||||
// GL_LAST_VERTEX_CONVENTION for both backends, derived from nothing, and wrong on both test
|
||||
// devices in opposite directions. DirectGLES now forwards what its loader resolved; DirectVulkan
|
||||
@@ -1234,6 +1298,176 @@ void main() {
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// THE invariant every KHR-GL45.limits.* case checks, in one place. When the conformance table
|
||||
// gives a limit both a glGetIntegerv pname and a GLSL built-in constant, it reads the query and
|
||||
// then compiles a shader that writes the built-in into an SSBO and demands EXACT equality - so a
|
||||
// limit answered from two unreconciled tables fails the SECOND half of the case, with a message
|
||||
// about a number rather than about the two tables. Seven of them did: gl_MaxVertexAttribs said 64
|
||||
// against a query of 32, gl_MaxDrawBuffers 32 against 8, gl_MaxCombinedTextureImageUnits 80
|
||||
// against 96, gl_MaxVaryingComponents 60 against 64, gl_MaxCombinedShaderOutputResources 8
|
||||
// against 29.
|
||||
//
|
||||
// KEEP THIS TABLE GROWING. Every pname added to GL_Getter that also has a gl_Max* built-in
|
||||
// belongs here; that is what stops the next one from drifting.
|
||||
TEST(GetterSanity, EveryLimitWithABuiltinAgreesWithItsQuery) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
MG_Backend::pActiveBackendObject =
|
||||
MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{}, BackendType::DirectGLES);
|
||||
|
||||
struct LimitPair {
|
||||
GLenum pname;
|
||||
const char* builtin;
|
||||
};
|
||||
const LimitPair pairs[] = {
|
||||
{GL_MAX_VERTEX_ATTRIBS, "gl_MaxVertexAttribs"},
|
||||
{GL_MAX_VERTEX_UNIFORM_COMPONENTS, "gl_MaxVertexUniformComponents"},
|
||||
{GL_MAX_VERTEX_UNIFORM_VECTORS, "gl_MaxVertexUniformVectors"},
|
||||
{GL_MAX_VERTEX_OUTPUT_COMPONENTS, "gl_MaxVertexOutputComponents"},
|
||||
{GL_MAX_VARYING_COMPONENTS, "gl_MaxVaryingComponents"},
|
||||
{GL_MAX_VARYING_VECTORS, "gl_MaxVaryingVectors"},
|
||||
{GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, "gl_MaxVertexTextureImageUnits"},
|
||||
{GL_MAX_TEXTURE_IMAGE_UNITS, "gl_MaxTextureImageUnits"},
|
||||
{GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, "gl_MaxCombinedTextureImageUnits"},
|
||||
{GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, "gl_MaxFragmentUniformComponents"},
|
||||
{GL_MAX_FRAGMENT_UNIFORM_VECTORS, "gl_MaxFragmentUniformVectors"},
|
||||
{GL_MAX_FRAGMENT_INPUT_COMPONENTS, "gl_MaxFragmentInputComponents"},
|
||||
{GL_MAX_DRAW_BUFFERS, "gl_MaxDrawBuffers"},
|
||||
{GL_MAX_IMAGE_UNITS, "gl_MaxImageUnits"},
|
||||
// The SAME token (0x8F39) under two spellings, and the two glslang fields behind them
|
||||
// must therefore carry the same value.
|
||||
{GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS, "gl_MaxCombinedImageUnitsAndFragmentOutputs"},
|
||||
{GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES, "gl_MaxCombinedShaderOutputResources"},
|
||||
{GL_MAX_CLIP_DISTANCES, "gl_MaxClipDistances"},
|
||||
{GL_MAX_CULL_DISTANCES, "gl_MaxCullDistances"},
|
||||
{GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, "gl_MaxCombinedClipAndCullDistances"},
|
||||
{GL_MAX_SAMPLES, "gl_MaxSamples"},
|
||||
{GL_MIN_PROGRAM_TEXEL_OFFSET, "gl_MinProgramTexelOffset"},
|
||||
{GL_MAX_PROGRAM_TEXEL_OFFSET, "gl_MaxProgramTexelOffset"},
|
||||
{GL_MAX_GEOMETRY_INPUT_COMPONENTS, "gl_MaxGeometryInputComponents"},
|
||||
{GL_MAX_GEOMETRY_OUTPUT_COMPONENTS, "gl_MaxGeometryOutputComponents"},
|
||||
{GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS, "gl_MaxGeometryTextureImageUnits"},
|
||||
{GL_MAX_GEOMETRY_OUTPUT_VERTICES, "gl_MaxGeometryOutputVertices"},
|
||||
{GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS, "gl_MaxGeometryTotalOutputComponents"},
|
||||
{GL_MAX_GEOMETRY_UNIFORM_COMPONENTS, "gl_MaxGeometryUniformComponents"},
|
||||
{GL_MAX_PATCH_VERTICES, "gl_MaxPatchVertices"},
|
||||
{GL_MAX_TESS_GEN_LEVEL, "gl_MaxTessGenLevel"},
|
||||
{GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "gl_MaxTessControlInputComponents"},
|
||||
{GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "gl_MaxTessControlOutputComponents"},
|
||||
{GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "gl_MaxTessControlTextureImageUnits"},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "gl_MaxTessControlUniformComponents"},
|
||||
{GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "gl_MaxTessControlTotalOutputComponents"},
|
||||
{GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "gl_MaxTessEvaluationInputComponents"},
|
||||
{GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "gl_MaxTessEvaluationOutputComponents"},
|
||||
{GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "gl_MaxTessEvaluationTextureImageUnits"},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "gl_MaxTessEvaluationUniformComponents"},
|
||||
{GL_MAX_TESS_PATCH_COMPONENTS, "gl_MaxTessPatchComponents"},
|
||||
{GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, "gl_MaxTransformFeedbackBuffers"},
|
||||
{GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, "gl_MaxTransformFeedbackInterleavedComponents"},
|
||||
// gl_MaxAtomicCounterBindings is glslang's name for the binding count; the GL spelling is
|
||||
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS.
|
||||
{GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, "gl_MaxAtomicCounterBindings"},
|
||||
{GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, "gl_MaxAtomicCounterBufferSize"},
|
||||
};
|
||||
|
||||
// The compile runs against a captured env, exactly as the pipeline's does - that is what
|
||||
// makes "the resource table" mean the same thing here as it does in production.
|
||||
const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv();
|
||||
for (const LimitPair& pair : pairs) {
|
||||
GLint reported = -424242;
|
||||
MG_Impl::GLImpl::GetIntegerv(pair.pname, &reported);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
|
||||
<< pair.builtin << "'s pname is not answerable at all";
|
||||
|
||||
// A negative array size is a compile error, so the stage only compiles when the built-in
|
||||
// equals what the query just reported. Two-sided by construction: a resource table that
|
||||
// is too permissive fails it exactly like one that is too tight. One shader per pair, so
|
||||
// a failure names the limit instead of reporting "something disagreed".
|
||||
const String source = String("#version 460 core\nout vec4 mgColor;\nconst int mgAgree = (") +
|
||||
pair.builtin + " == " + std::to_string(reported) +
|
||||
") ? 1 : -1;\nint mgProbe[mgAgree];\nvoid main() { mgProbe[0] = 0; mgColor = "
|
||||
"vec4(float(mgProbe[0])); }\n";
|
||||
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||
.shaderType = GL_FRAGMENT_SHADER,
|
||||
.sourceStr = source,
|
||||
.env = env.get(),
|
||||
});
|
||||
EXPECT_TRUE(compiled) << pair.builtin << " does not equal glGetIntegerv's " << reported << ":\n"
|
||||
<< (compiled ? String() : compiled.error().log);
|
||||
}
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// GL_MAX_ELEMENT_INDEX is 64-bit state whose required value (2^32-1) does not fit a GLint, so it
|
||||
// needs its own case in BOTH widths: the 64-bit query has to answer 4294967295 and the 32-bit one
|
||||
// has to saturate, per the GL state-query conversion rules. It used to be a single `1024 * 1024;
|
||||
// // TODO` in the 32-bit table, and glGetInteger64v - which is how the conformance suite reads it
|
||||
// - widened that.
|
||||
TEST(GetterSanity, MaxElementIndexIsTheFull32BitIndexCeiling) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
|
||||
|
||||
GLint64 wide = -1;
|
||||
MG_Impl::GLImpl::GetInteger64v(GL_MAX_ELEMENT_INDEX, &wide);
|
||||
EXPECT_EQ(wide, static_cast<GLint64>(0xFFFFFFFFLL));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint narrow = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ELEMENT_INDEX, &narrow);
|
||||
EXPECT_EQ(narrow, INT32_MAX) << "the 32-bit query must saturate, not truncate or wrap";
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 gives GL_MAX_SAMPLES a minimum of four and the three per-category
|
||||
// ceilings a minimum of ONE. Flooring the latter at four is the advertised-caps lie that made
|
||||
// KHR-GL46.sample_variables.mask.rgba8i run at all: the frontend promised four integer samples,
|
||||
// the backend clamped the realised allocation to the one the driver can back, and the application
|
||||
// wrote per-sample data it could never read.
|
||||
TEST(GetterSanity, PerCategoryMultisampleCeilingsAreProbedRatherThanFlooredAtFour) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
|
||||
MG_Backend::DynamicBackendParameters params;
|
||||
params.MaxSamples = 4;
|
||||
params.MaxColorTextureSamples = 4;
|
||||
params.MaxDepthTextureSamples = 2;
|
||||
params.MaxIntegerSamples = 1;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
|
||||
GLint reported = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_INTEGER_SAMPLES, &reported);
|
||||
EXPECT_EQ(reported, 1) << "an integer multisample texture is backed by one sample here, and "
|
||||
"saying otherwise is what the application allocates against";
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEPTH_TEXTURE_SAMPLES, &reported);
|
||||
EXPECT_EQ(reported, 2);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COLOR_TEXTURE_SAMPLES, &reported);
|
||||
EXPECT_EQ(reported, 4);
|
||||
// ...while GL_MAX_SAMPLES keeps its floor of four, which is the one the spec really requires.
|
||||
params.MaxSamples = 1;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_SAMPLES, &reported);
|
||||
EXPECT_EQ(reported, 4);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
|
||||
using namespace MobileGL;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user