mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Merge] (ShaderTranspiler, GLState, DirectGLES): land dev GL43 wave2/wave3 under the translation cache
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/BaseInstanceInjectionTest.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// The gate on the gl_BaseInstance indirect lowering in
|
||||
// MG_Backend/DirectGLES/Managers.cpp. That lowering declares a std430 storage block in the
|
||||
// VERTEX stage, and a vertex-stage storage block is optional in both APIs: the minimum for
|
||||
// GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2 table 21.44), and ARM's
|
||||
// GLES driver takes that allowance - a Mali-G925-Immortalis reports 0 for it and for all three
|
||||
// other graphics stages.
|
||||
//
|
||||
// Emitting the block on such a driver does not make it work. The driver refuses the program at
|
||||
// link time ("The number of vertex shader storage blocks (1) is greater than the maximum number
|
||||
// allowed (0)"), and because MobileGL's frontend GL_LINK_STATUS is glslang's rather than the
|
||||
// driver's, the application is told the program linked and then every draw with it renders
|
||||
// nothing. Dropping the indirect half instead keeps ordinary draws working and costs only the
|
||||
// per-command baseInstance of an indirect draw.
|
||||
//
|
||||
// No GL context and no driver: the lowering is a pure String -> String pass over one capability.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
|
||||
using MobileGL::Bool;
|
||||
using MobileGL::String;
|
||||
using MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
||||
using MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms;
|
||||
using MobileGL::MG_Backend::DirectGLES::VertexStageStorageBlockUsable;
|
||||
|
||||
namespace {
|
||||
// The capability block is a process-global the backend fills in at init; restore whatever
|
||||
// was there so ordering between this suite and any other that touches it cannot matter.
|
||||
struct ScopedGLESCapabilitiesOverride {
|
||||
ScopedGLESCapabilitiesOverride(): saved(g_GLESCapabilities) {}
|
||||
~ScopedGLESCapabilitiesOverride() { g_GLESCapabilities = saved; }
|
||||
ScopedGLESCapabilitiesOverride(const ScopedGLESCapabilitiesOverride&) = delete;
|
||||
ScopedGLESCapabilitiesOverride& operator=(const ScopedGLESCapabilitiesOverride&) = delete;
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities saved;
|
||||
};
|
||||
|
||||
Bool Contains(const String& haystack, const String& needle) {
|
||||
return haystack.find(needle) != String::npos;
|
||||
}
|
||||
|
||||
// What SPIRV-Cross hands the backend after LowerDrawParametersPass has demoted
|
||||
// gl_BaseInstance to a Private global.
|
||||
constexpr const char* kLoweredBaseInstanceVertexShader = R"(#version 310 es
|
||||
highp int mg_BaseInstanceLowered;
|
||||
void main() {
|
||||
int instance = gl_InstanceID + mg_BaseInstanceLowered;
|
||||
gl_Position = vec4(float(instance));
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
// One block is all the indirect view needs, so the predicate is a >= 1 test.
|
||||
TEST(VertexStageStorageBlockUsableTest, RequiresAtLeastOneBlock) {
|
||||
EXPECT_FALSE(VertexStageStorageBlockUsable(0));
|
||||
EXPECT_TRUE(VertexStageStorageBlockUsable(1));
|
||||
EXPECT_TRUE(VertexStageStorageBlockUsable(16));
|
||||
}
|
||||
|
||||
// A driver that leaves the out-param untouched tells us nothing, and guessing "yes" is exactly
|
||||
// what produces the unlinkable program. Unusable, not clamped up to one.
|
||||
TEST(VertexStageStorageBlockUsableTest, ANegativeCountIsUnusableRatherThanClamped) {
|
||||
EXPECT_FALSE(VertexStageStorageBlockUsable(-1));
|
||||
EXPECT_FALSE(VertexStageStorageBlockUsable(-2147483647 - 1));
|
||||
}
|
||||
|
||||
TEST(BaseInstanceInjectionGate, DriverWithVertexStorageBlocksGetsTheIndirectView) {
|
||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 1;
|
||||
|
||||
const String rewritten =
|
||||
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
||||
|
||||
EXPECT_TRUE(Contains(rewritten, "layout(std430, binding = 12) readonly buffer mg_IndirectParams"));
|
||||
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstanceWordIndex;"));
|
||||
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered ((mg_BaseInstanceWordIndex > 0) ? "
|
||||
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : mg_BaseInstance)"))
|
||||
<< rewritten;
|
||||
}
|
||||
|
||||
// The bug this gate exists for. The block must not appear at all - not at a different binding,
|
||||
// not behind a preprocessor guard: a declaration the driver counts is a declaration that makes
|
||||
// the whole program unlinkable, and the frontend never surfaces that failure.
|
||||
TEST(BaseInstanceInjectionGate, DriverWithoutVertexStorageBlocksDeclaresNoBlockAtAll) {
|
||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
||||
|
||||
const String rewritten =
|
||||
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
||||
|
||||
EXPECT_FALSE(Contains(rewritten, "mg_IndirectParams")) << rewritten;
|
||||
EXPECT_FALSE(Contains(rewritten, "buffer"));
|
||||
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
|
||||
// Nothing reads the word index any more, so nothing may declare it either - its presence is
|
||||
// what BackendProgramObjectImpl uses to decide whether to bind an indirect params buffer.
|
||||
EXPECT_FALSE(Contains(rewritten, "mg_BaseInstanceWordIndex"));
|
||||
}
|
||||
|
||||
// Degraded, but still correct for every non-indirect draw: the plain mg_BaseInstance uniform is
|
||||
// what the non-indirect draw entry points already write.
|
||||
TEST(BaseInstanceInjectionGate, WithoutTheBlockBaseInstanceFallsBackToThePlainUniform) {
|
||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
||||
|
||||
const String rewritten =
|
||||
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
||||
|
||||
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstance;")) << rewritten;
|
||||
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered (mg_BaseInstance)")) << rewritten;
|
||||
// The global declaration must be gone; leaving it would shadow the define.
|
||||
EXPECT_FALSE(Contains(rewritten, "highp int mg_BaseInstanceLowered;\n"));
|
||||
}
|
||||
|
||||
// On a driver that both leaks baseInstance into gl_InstanceID and has no vertex storage block,
|
||||
// the rebase has nothing to subtract. Subtracting the uniform instead would remove the base
|
||||
// twice from every non-indirect draw, which is worse than not rebasing at all.
|
||||
TEST(BaseInstanceInjectionGate, WithoutTheBlockInstanceIdRebaseCollapsesToIdentity) {
|
||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = true;
|
||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
||||
|
||||
const String rewritten =
|
||||
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
||||
|
||||
EXPECT_TRUE(Contains(rewritten, "#define mg_ZeroBasedInstanceID gl_InstanceID")) << rewritten;
|
||||
EXPECT_FALSE(Contains(rewritten, "gl_InstanceID - ("));
|
||||
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
|
||||
}
|
||||
|
||||
// The gate is scoped to the block, not to the whole pass: mg_DrawID and mg_BaseVertex are plain
|
||||
// uniforms with no storage block behind them and must still be promoted on such a driver.
|
||||
TEST(BaseInstanceInjectionGate, DrawIdAndBaseVertexArePromotedRegardless) {
|
||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
||||
|
||||
const String source = R"(#version 310 es
|
||||
highp int mg_DrawID;
|
||||
highp int mg_BaseVertex;
|
||||
void main() {
|
||||
gl_Position = vec4(float(mg_DrawID + mg_BaseVertex));
|
||||
}
|
||||
)";
|
||||
|
||||
const String rewritten = PromoteDrawParameterGlobalsToUniforms(source, GL_VERTEX_SHADER);
|
||||
|
||||
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_DrawID;")) << rewritten;
|
||||
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseVertex;")) << rewritten;
|
||||
}
|
||||
@@ -16,5 +16,22 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_executable(
|
||||
BaseInstanceInjectionTest
|
||||
BaseInstanceInjectionTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(BaseInstanceInjectionTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
BaseInstanceInjectionTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(BaseInstanceInjectionTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -59,9 +59,11 @@ void main()
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
|
||||
// Both halves: same binding, same format, same type - which is what makes two image
|
||||
// variables on one image unit legal.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform readonly highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||
// variables on one image unit legal - and both `coherent`, which is what makes the store
|
||||
// through one of them visible to the load through the other.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(
|
||||
out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||
|
||||
// The load keeps the original name, the store moves to the writeonly half.
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
|
||||
@@ -152,9 +154,9 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform readonly highp image2D gohan[3];"));
|
||||
EXPECT_TRUE(Contains(out,
|
||||
"layout(binding = 6, rgba8) uniform writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D gohan[3];"));
|
||||
EXPECT_TRUE(Contains(
|
||||
out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],"));
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
|
||||
}
|
||||
@@ -174,9 +176,11 @@ void main()
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
|
||||
// goku is read+write -> split; goku_hd is write-only -> qualified in place, not split.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform readonly highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||
// goku is read+write -> split (and coherent with it); goku_hd is write-only -> qualified in
|
||||
// place, not split, and left non-coherent because nothing aliases it.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(
|
||||
out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
|
||||
EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd"));
|
||||
@@ -197,6 +201,36 @@ void main()
|
||||
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;"));
|
||||
EXPECT_TRUE(
|
||||
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";"));
|
||||
// ...and the coherent the split adds is not a SECOND one: a repeated memory qualifier is a
|
||||
// compile error in ESSL, so the source's own has to be recognized.
|
||||
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
||||
}
|
||||
|
||||
// The visibility half of the split, and the reason it is not cosmetic: GLSL orders a
|
||||
// same-variable read-after-write within one invocation by construction, but once the store goes
|
||||
// through `mg_imageWrite_goku` and the load through `goku` the two are DIFFERENT variables, and
|
||||
// the ordering only holds if both are coherent. Desktop sources almost never say so - they had
|
||||
// no reason to - which is how KHR-GL4x.shader_image_load_store.advanced-memory-order's
|
||||
// store/load/compare loop started reading back the value it had not stored yet.
|
||||
TEST(SplitReadWriteImageUniformsTest, SplitPairIsMadeCoherentEvenWhenTheSourceIsNot) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 2, rgba8) uniform highp image2D goku;
|
||||
layout(binding = 3, rgba8) uniform highp image2D storeOnly;
|
||||
layout(location = 0) out highp vec4 mg_FragColor;
|
||||
void main()
|
||||
{
|
||||
imageStore(goku, ivec2(0), vec4(1.0));
|
||||
mg_FragColor = imageLoad(goku, ivec2(0));
|
||||
imageStore(storeOnly, ivec2(0), vec4(2.0));
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D goku;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")) << out;
|
||||
// Exactly the two halves of the pair, and nothing else: the store-only image is repaired in
|
||||
// place, has no alias to stay visible to, and must not pay for uncached access.
|
||||
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
||||
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
|
||||
}
|
||||
|
||||
// imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps
|
||||
|
||||
@@ -34,6 +34,17 @@ namespace {
|
||||
GLint maxFragmentImageUniforms = 4;
|
||||
GLint maxComputeImageUniforms = 5;
|
||||
bool maxGeometryImageUniformsQueried = false;
|
||||
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. The vertex and fragment pnames are ES 3.1,
|
||||
// but the tessellation and geometry ones only exist from ES 3.2 on, so asking for them
|
||||
// on an older context raises GL_INVALID_ENUM - the same shape as the buffer-texture and
|
||||
// anisotropy probes. The "queried" flags are what pin that gating; the "raises error"
|
||||
// knob is what pins the drain.
|
||||
GLint maxTessControlSsboBlocks = 6;
|
||||
GLint maxTessEvaluationSsboBlocks = 7;
|
||||
GLint maxGeometrySsboBlocks = 8;
|
||||
GLint maxFragmentSsboBlocks = 9;
|
||||
bool tessAndGeometrySsboBlocksQueried = false;
|
||||
bool perStageSsboBlockQueryRaisesError = false;
|
||||
GLfloat minFragmentInterpolationOffset = -0.75f;
|
||||
GLfloat maxFragmentInterpolationOffset = 0.625f;
|
||||
GLint fragmentInterpolationOffsetBits = 6;
|
||||
@@ -111,7 +122,30 @@ namespace {
|
||||
funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
|
||||
switch (pname) {
|
||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||
*data = g_fake.maxVertexSsboBlocks;
|
||||
if (g_fake.perStageSsboBlockQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
*data = g_fake.maxVertexSsboBlocks;
|
||||
}
|
||||
break;
|
||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
||||
if (g_fake.perStageSsboBlockQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
*data = g_fake.maxFragmentSsboBlocks;
|
||||
}
|
||||
break;
|
||||
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
||||
g_fake.tessAndGeometrySsboBlocksQueried = true;
|
||||
*data = g_fake.maxTessControlSsboBlocks;
|
||||
break;
|
||||
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
||||
g_fake.tessAndGeometrySsboBlocksQueried = true;
|
||||
*data = g_fake.maxTessEvaluationSsboBlocks;
|
||||
break;
|
||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||
g_fake.tessAndGeometrySsboBlocksQueried = true;
|
||||
*data = g_fake.maxGeometrySsboBlocks;
|
||||
break;
|
||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxVertexImageUniforms;
|
||||
@@ -400,6 +434,10 @@ namespace {
|
||||
MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() {
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
caps.GLESVersion = {3, 1, 0};
|
||||
// The probe reads its vertex storage-block gate from caps rather than re-querying the
|
||||
// driver (FillInGLESCapabilities resolves the per-stage limits before calling it), so a
|
||||
// caps struct handed to the probe directly has to carry what the fake reports.
|
||||
caps.MaxVertexShaderStorageBlocks = g_fake.maxVertexSsboBlocks;
|
||||
return caps;
|
||||
}
|
||||
|
||||
@@ -527,6 +565,83 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
|
||||
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
|
||||
}
|
||||
|
||||
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS probes. These decide whether an application is
|
||||
// told it may declare a storage block in a graphics stage, and on a driver that cannot serve one
|
||||
// a wrong answer is not a cosmetic mis-report: the program is built, the driver refuses it at
|
||||
// link time, the frontend reports LINK_STATUS true anyway, and every draw with it renders
|
||||
// nothing. A Mali-G925-Immortalis reports 0 for vertex, both tessellation stages and geometry.
|
||||
TEST(PerStageStorageBlockCapabilities, TakesTheDriverValuesAndGatesTessAndGeometryOnEs32) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
// ES 3.1: the tessellation and geometry pnames do not exist, so they must not be asked for
|
||||
// and the stages must report the spec minimum of 0 rather than a hopeful driver number.
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 3;
|
||||
MobileGL::MG_External::GLESCapabilities es31Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
|
||||
EXPECT_EQ(es31Caps.MaxVertexShaderStorageBlocks, 3);
|
||||
EXPECT_EQ(es31Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
|
||||
EXPECT_EQ(es31Caps.MaxTessControlShaderStorageBlocks, 0);
|
||||
EXPECT_EQ(es31Caps.MaxTessEvaluationShaderStorageBlocks, 0);
|
||||
EXPECT_EQ(es31Caps.MaxGeometryShaderStorageBlocks, 0);
|
||||
EXPECT_FALSE(g_fake.tessAndGeometrySsboBlocksQueried);
|
||||
|
||||
// ES 3.2: all five are real pnames and all five driver values must come through verbatim.
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 3;
|
||||
g_fake.glesMinorVersion = 2;
|
||||
MobileGL::MG_External::GLESCapabilities es32Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
|
||||
EXPECT_EQ(es32Caps.MaxVertexShaderStorageBlocks, 3);
|
||||
EXPECT_EQ(es32Caps.MaxTessControlShaderStorageBlocks, g_fake.maxTessControlSsboBlocks);
|
||||
EXPECT_EQ(es32Caps.MaxTessEvaluationShaderStorageBlocks, g_fake.maxTessEvaluationSsboBlocks);
|
||||
EXPECT_EQ(es32Caps.MaxGeometryShaderStorageBlocks, g_fake.maxGeometrySsboBlocks);
|
||||
EXPECT_EQ(es32Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
|
||||
EXPECT_TRUE(g_fake.tessAndGeometrySsboBlocksQueried);
|
||||
}
|
||||
|
||||
// Zero has to survive the round trip intact. It is the answer that matters most - it is what
|
||||
// ARM's driver actually reports - so a probe that silently substituted a floor would put the
|
||||
// bug straight back.
|
||||
TEST(PerStageStorageBlockCapabilities, AZeroFromTheDriverIsReportedAsZero) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.glesMinorVersion = 2;
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.maxTessControlSsboBlocks = 0;
|
||||
g_fake.maxTessEvaluationSsboBlocks = 0;
|
||||
g_fake.maxGeometrySsboBlocks = 0;
|
||||
g_fake.maxFragmentSsboBlocks = 16;
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities maliLikeCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(maliLikeCaps, funcs));
|
||||
|
||||
EXPECT_EQ(maliLikeCaps.MaxVertexShaderStorageBlocks, 0);
|
||||
EXPECT_EQ(maliLikeCaps.MaxTessControlShaderStorageBlocks, 0);
|
||||
EXPECT_EQ(maliLikeCaps.MaxTessEvaluationShaderStorageBlocks, 0);
|
||||
EXPECT_EQ(maliLikeCaps.MaxGeometryShaderStorageBlocks, 0);
|
||||
EXPECT_EQ(maliLikeCaps.MaxFragmentShaderStorageBlocks, 16);
|
||||
}
|
||||
|
||||
// A rejected query must leave no error behind for the application's first glGetError to find,
|
||||
// and must fall back to the spec minimums rather than to whatever the untouched out-param held.
|
||||
TEST(PerStageStorageBlockCapabilities, ARejectedQueryIsDrainedAndFallsBackToTheSpecMinimums) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.perStageSsboBlockQueryRaisesError = true;
|
||||
g_fake.maxVertexSsboBlocks = 12;
|
||||
g_fake.maxFragmentSsboBlocks = 12;
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
|
||||
EXPECT_EQ(caps.MaxVertexShaderStorageBlocks, 0);
|
||||
EXPECT_EQ(caps.MaxFragmentShaderStorageBlocks, 4);
|
||||
EXPECT_EQ(g_fake.pendingError, static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
|
||||
@@ -716,6 +716,177 @@ void main() {
|
||||
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
|
||||
}
|
||||
|
||||
// Two counters that share a binding AND an offset must fail to link. glslang's own check
|
||||
// lives in fixOffset(), which the Vulkan-relaxed parse never reaches - it folds the
|
||||
// atomic_uint into a storage block and returns from declareVariable() first - so the pair
|
||||
// used to link cleanly and then increment the same four bytes.
|
||||
TEST_F(ProgramInterfaceTest, OverlappingAtomicCounterOffsetsFailToLink) {
|
||||
const char* fs = R"(#version 430
|
||||
out vec4 color;
|
||||
layout (binding = 0, offset = 0) uniform atomic_uint a;
|
||||
layout (binding = 0, offset = 0) uniform atomic_uint b;
|
||||
void main() { color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b))); }
|
||||
)";
|
||||
const GLuint p = MakeProgram(kSimpleVs, fs);
|
||||
LinkProgram(p);
|
||||
GLint status = -1;
|
||||
GetProgramiv(p, GL_LINK_STATUS, &status);
|
||||
EXPECT_EQ(status, GL_FALSE);
|
||||
char log[4096] = "";
|
||||
GetProgramInfoLog(p, sizeof(log), nullptr, log);
|
||||
EXPECT_NE(std::string(log).find("overlap"), std::string::npos) << "info log was: " << log;
|
||||
ClearErrors();
|
||||
|
||||
// Distinct offsets at one binding, and the same offset at two different bindings, are
|
||||
// both legal and must still link - a check keyed any wider would reject them.
|
||||
const char* legalFs = R"(#version 430
|
||||
out vec4 color;
|
||||
layout (binding = 0, offset = 0) uniform atomic_uint a;
|
||||
layout (binding = 0, offset = 4) uniform atomic_uint b;
|
||||
layout (binding = 1, offset = 0) uniform atomic_uint c;
|
||||
void main() {
|
||||
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
|
||||
}
|
||||
)";
|
||||
const GLuint legal = MakeProgram(kSimpleVs, legalFs);
|
||||
LinkProgram(legal);
|
||||
ExpectLinked(legal);
|
||||
ClearErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6 fails the link when a stage's active image uniforms exceed
|
||||
// GL_MAX_*_IMAGE_UNIFORMS, or when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing
|
||||
// counted them - glslang keeps those numbers only so gl_Max*ImageUniforms can expand from
|
||||
// them - so every deliberately-oversized program in
|
||||
// KHR-GL4x.shader_image_load_store.uniform-limits linked cleanly and then rendered nothing.
|
||||
//
|
||||
// Sized off the ADVERTISED limits rather than a constant, because the numbers come from the
|
||||
// active backend and the whole point of the check is that the two agree.
|
||||
TEST_F(ProgramInterfaceTest, ImageUniformsOverAStageLimitFailToLink) {
|
||||
GLint maxFragmentImages = 0;
|
||||
GLint maxCombinedImages = 0;
|
||||
GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages);
|
||||
GetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImages);
|
||||
ClearErrors();
|
||||
ASSERT_GT(maxFragmentImages, 0);
|
||||
|
||||
// The fragment stage is compiled explicitly so a COMPILE failure can never be mistaken
|
||||
// for the link failure under test.
|
||||
const auto linkWithFragmentImages = [](GLint count) {
|
||||
const std::string n = std::to_string(count);
|
||||
const std::string source = std::string(R"(#version 430
|
||||
out vec4 color;
|
||||
layout(r32i) uniform iimage2D u_image[)") + n + R"(];
|
||||
void main() {
|
||||
int value = 1;
|
||||
for (int i = 0; i < )" + n + R"(; ++i) {
|
||||
value = imageAtomicAdd(u_image[i], ivec2(0), value);
|
||||
}
|
||||
color = vec4(float(value));
|
||||
}
|
||||
)";
|
||||
const char* sourcePtr = source.c_str();
|
||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &sourcePtr, nullptr);
|
||||
CompileShader(fs);
|
||||
GLint compiled = 0;
|
||||
GetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
|
||||
EXPECT_EQ(compiled, GL_TRUE) << "the fragment stage with " << count << " image uniforms must compile";
|
||||
|
||||
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||
ShaderSource(vs, 1, &kSimpleVs, nullptr);
|
||||
CompileShader(vs);
|
||||
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, vs);
|
||||
AttachShader(program, fs);
|
||||
LinkProgram(program);
|
||||
return program;
|
||||
};
|
||||
|
||||
const GLuint over = linkWithFragmentImages(maxFragmentImages + 1);
|
||||
GLint status = -1;
|
||||
GetProgramiv(over, GL_LINK_STATUS, &status);
|
||||
EXPECT_EQ(status, GL_FALSE);
|
||||
char log[4096] = "";
|
||||
GetProgramInfoLog(over, sizeof(log), nullptr, log);
|
||||
EXPECT_NE(std::string(log).find("GL_MAX_FRAGMENT_IMAGE_UNIFORMS"), std::string::npos)
|
||||
<< "info log was: " << log;
|
||||
ClearErrors();
|
||||
|
||||
// Exactly AT the limit is legal and must still link: the comparison is strictly
|
||||
// greater-than, and the conformance suite's combined-stage subcase builds a program that
|
||||
// fills every stage to its own limit and expects it to link whenever the combined limit
|
||||
// can hold them.
|
||||
if (maxFragmentImages <= maxCombinedImages) {
|
||||
const GLuint atLimit = linkWithFragmentImages(maxFragmentImages);
|
||||
ExpectLinked(atLimit);
|
||||
ClearErrors();
|
||||
}
|
||||
}
|
||||
|
||||
// glGetProgramiv(GL_ACTIVE_ATOMIC_COUNTER_BUFFERS) and glGetActiveAtomicCounterBufferiv are
|
||||
// the pre-4.3 spelling of the interface above, and the spec requires the two to agree.
|
||||
// Neither did: the first counted glslang's atomic counter UNIFORMS - zero, because the
|
||||
// relaxed parse folds every atomic_uint into a storage block before reflection runs - and
|
||||
// the second was a stub that wrote nothing and raised nothing.
|
||||
TEST_F(ProgramInterfaceTest, ActiveAtomicCounterBufferQueriesMatchTheInterface) {
|
||||
const char* fs = R"(#version 430
|
||||
out vec4 color;
|
||||
layout (binding = 1, offset = 0) uniform atomic_uint a;
|
||||
layout (binding = 2, offset = 0) uniform atomic_uint b;
|
||||
layout (binding = 2, offset = 4) uniform atomic_uint c;
|
||||
void main() {
|
||||
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
|
||||
}
|
||||
)";
|
||||
const GLuint p = MakeProgram(kSimpleVs, fs);
|
||||
LinkProgram(p);
|
||||
ExpectLinked(p);
|
||||
ClearErrors();
|
||||
|
||||
GLint bufferCount = -12345;
|
||||
GetProgramiv(p, GL_ACTIVE_ATOMIC_COUNTER_BUFFERS, &bufferCount);
|
||||
EXPECT_EQ(bufferCount, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
|
||||
ASSERT_EQ(bufferCount, 2);
|
||||
|
||||
const auto activeBufferiv = [p](GLuint index, GLenum pname) {
|
||||
GLint value = -12345;
|
||||
GetActiveAtomicCounterBufferiv(p, index, pname, &value);
|
||||
return value;
|
||||
};
|
||||
for (GLuint index = 0; index < static_cast<GLuint>(bufferCount); ++index) {
|
||||
const std::vector<GLint> viaInterface =
|
||||
Props(p, GL_ATOMIC_COUNTER_BUFFER, index,
|
||||
{GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES,
|
||||
GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER});
|
||||
ASSERT_EQ(viaInterface.size(), 5u);
|
||||
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_BINDING), viaInterface[0]);
|
||||
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE), viaInterface[1]);
|
||||
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS), viaInterface[2]);
|
||||
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER), viaInterface[3]);
|
||||
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER), viaInterface[4]);
|
||||
|
||||
// The counter indices are the GL_UNIFORM indices, in the same order.
|
||||
const std::vector<GLint> expectedIndices = Props(p, GL_ATOMIC_COUNTER_BUFFER, index, {GL_ACTIVE_VARIABLES});
|
||||
ASSERT_FALSE(expectedIndices.empty());
|
||||
std::vector<GLint> indices(expectedIndices.size(), -12345);
|
||||
GetActiveAtomicCounterBufferiv(p, index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES,
|
||||
indices.data());
|
||||
EXPECT_EQ(indices, expectedIndices);
|
||||
}
|
||||
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||
|
||||
GLint sink = -12345;
|
||||
GetActiveAtomicCounterBufferiv(p, static_cast<GLuint>(bufferCount), GL_ATOMIC_COUNTER_BUFFER_BINDING, &sink);
|
||||
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
|
||||
EXPECT_EQ(sink, -12345) << "a rejected query must not write the caller's output";
|
||||
// The interface-query spelling of the same property is NOT accepted here.
|
||||
GetActiveAtomicCounterBufferiv(p, 0, GL_BUFFER_BINDING, &sink);
|
||||
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
|
||||
EXPECT_EQ(sink, -12345);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- transform-feedback ------
|
||||
TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) {
|
||||
const char* vs = R"(#version 430
|
||||
|
||||
@@ -3239,3 +3239,76 @@ TEST_F(ProgramTest, CreateShaderAndCreateShaderProgramvReportTheRightErrorClasse
|
||||
EXPECT_NE(program, 0u);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ARB_explicit_uniform_location / GL 4.6 core 7.6.1: a `layout(location = N)` uniform reserves N
|
||||
// EVEN WHEN IT IS INACTIVE. Dead default-block uniforms are correctly filtered off the GL surface
|
||||
// (glGetUniformLocation must answer -1 for them), but the implicit allocator used to walk straight
|
||||
// over the location they claimed and hand it to a uniform that never asked for it
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit3).
|
||||
TEST_F(ProgramTest, InactiveExplicitUniformLocationIsStillReserved) {
|
||||
const char* vsSource = R"(#version 430 core
|
||||
layout(location = 2) uniform vec4 uDeadAtTwo;
|
||||
uniform vec4 uA;
|
||||
uniform vec4 uB;
|
||||
uniform vec4 uC;
|
||||
uniform vec4 uD;
|
||||
void main() { gl_Position = uA + uB + uC + uD; }
|
||||
)";
|
||||
const char* fsSource = R"(#version 430 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||
|
||||
// Reserving a location must not resurrect the uniform: it is still inactive to GL.
|
||||
EXPECT_EQ(GetUniformLocation(program, "uDeadAtTwo"), -1);
|
||||
|
||||
for (const char* name : {"uA", "uB", "uC", "uD"}) {
|
||||
const GLint location = GetUniformLocation(program, name);
|
||||
EXPECT_GE(location, 0) << name << " lost its implicit location";
|
||||
EXPECT_NE(location, 2) << name << " was handed the location uDeadAtTwo reserved";
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The GL_MAX_UNIFORM_LOCATIONS boundary, from both sides. MAX_UNIFORM_LOCATIONS - 1 is the LAST
|
||||
// LEGAL location: it has to link and read back verbatim
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-max), which is only true while the advertised
|
||||
// value and what the link accepts are the SAME number - the getter used to advertise one more
|
||||
// location than any shader could name.
|
||||
//
|
||||
// The over-the-ceiling half is asserted through an ARRAY, because that is the only spelling the
|
||||
// link gets to judge: a bare `layout(location = MAX)` is already a compile error inside glslang
|
||||
// ("location is too large"), while an array's base compiles fine and only its last element passes
|
||||
// the ceiling (...uniform-loc-negative-link-max-num-of-locations).
|
||||
TEST_F(ProgramTest, ExplicitUniformLocationsHonourMaxUniformLocations) {
|
||||
GLint maxLocations = 0;
|
||||
GetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &maxLocations);
|
||||
ASSERT_GE(maxLocations, 1024) << "GL 4.3 requires at least 1024 uniform locations";
|
||||
|
||||
const char* fsSource = R"(#version 430 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||
|
||||
{
|
||||
const String source = String("#version 430 core\nlayout(location = ") +
|
||||
std::to_string(maxLocations - 1) +
|
||||
") uniform vec4 uAtLimit;\nvoid main() { gl_Position = uAtLimit; }\n";
|
||||
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str());
|
||||
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||
EXPECT_EQ(GetUniformLocation(program, "uAtLimit"), maxLocations - 1)
|
||||
<< "the last location in the pool is legal and must come back verbatim";
|
||||
}
|
||||
{
|
||||
const String source = String("#version 430 core\nlayout(location = ") +
|
||||
std::to_string(maxLocations - 4) +
|
||||
") uniform vec4 uSpill[8];\nvoid main() { gl_Position = uSpill[0]; }\n";
|
||||
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str());
|
||||
(void)LinkVsFs(vs, fs, GL_FALSE);
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -3840,3 +3840,228 @@ TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
|
||||
}
|
||||
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-syntax iteration 6. glslang assigns a block's member
|
||||
// offsets at DECLARATION time, where a member array that is still unsized contributes zero bytes -
|
||||
// so `vec4 position01[]; vec4 position2;` put both members at offset 0 and the shader read
|
||||
// position01[0] where it asked for position2. The preprocessor sizes the non-final member from the
|
||||
// largest constant index the source uses, which is what the language says it means.
|
||||
TEST_F(ProgramUtilTest, ANonFinalUnsizedBufferBlockMemberIsSizedFromItsLargestConstantIndex) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String source = R"(#version 430 core
|
||||
layout(packed) coherent buffer Buffer {
|
||||
vec4 position01[];
|
||||
vec4 position2;
|
||||
} g_buffer;
|
||||
void main() {
|
||||
if (gl_VertexID == 0) gl_Position = g_buffer.position01[0];
|
||||
else if (gl_VertexID == 1) gl_Position = g_buffer.position01[1];
|
||||
else if (gl_VertexID == 2) gl_Position = g_buffer.position2;
|
||||
}
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Vertex, source);
|
||||
EXPECT_NE(source.find("vec4 position01[2];"), String::npos) << source;
|
||||
EXPECT_EQ(source.find("position01[];"), String::npos) << source;
|
||||
|
||||
// The LAST member of a storage block is a run-time sized array, which is legal and already
|
||||
// laid out correctly - sizing it would be a wire-format change, not a repair.
|
||||
String lastMember = R"(#version 430 core
|
||||
buffer Buffer {
|
||||
vec4 head;
|
||||
vec4 tail[];
|
||||
} g_buffer;
|
||||
void main() {
|
||||
gl_Position = g_buffer.tail[0] + g_buffer.tail[3];
|
||||
}
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Vertex, lastMember);
|
||||
EXPECT_NE(lastMember.find("vec4 tail[];"), String::npos) << lastMember;
|
||||
|
||||
// A member the shader subscripts with anything but a literal cannot be sized from the source,
|
||||
// so it is left exactly as it was.
|
||||
String dynamicIndex = R"(#version 430 core
|
||||
buffer Buffer {
|
||||
vec4 head[];
|
||||
vec4 tail;
|
||||
} g_buffer;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
gl_Position = g_buffer.head[g_index] + g_buffer.tail;
|
||||
}
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Vertex, dynamicIndex);
|
||||
EXPECT_NE(dynamicIndex.find("vec4 head[];"), String::npos) << dynamicIndex;
|
||||
|
||||
// `buffer` is also a member memory qualifier; a declaration that uses it must not be mistaken
|
||||
// for a block header.
|
||||
String memberQualifier = R"(#version 430 core
|
||||
coherent buffer Buffer {
|
||||
buffer vec4 position0;
|
||||
vec4 position1[];
|
||||
vec4 position2;
|
||||
} g_buffer;
|
||||
void main() {
|
||||
gl_Position = g_buffer.position0 + g_buffer.position1[2] + g_buffer.position2;
|
||||
}
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Vertex, memberQualifier);
|
||||
EXPECT_NE(memberQualifier.find("vec4 position1[3];"), String::npos) << memberQualifier;
|
||||
}
|
||||
|
||||
// KHR-GL43.shader_storage_buffer_object.negative-glsl-compileTime: a storage block declared at
|
||||
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS must fail to compile, and so must an arrayed one whose
|
||||
// LAST element passes the ceiling. The relaxed Vulkan-rules parse enforces neither.
|
||||
TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
constexpr Int kMaxBindings = 36;
|
||||
const auto violation = [](const String& body) {
|
||||
return FindShaderStorageBindingViolation("#version 430 core\n" + body + "void main() {}\n", kMaxBindings);
|
||||
};
|
||||
|
||||
// The boundary itself: max - 1 is the last legal point, max is one past it.
|
||||
EXPECT_FALSE(violation("layout(binding = 35) buffer Buffer { int x; };\n").has_value());
|
||||
EXPECT_TRUE(violation("layout(binding = 36) buffer Buffer { int x; };\n").has_value());
|
||||
|
||||
// An instance array takes CONSECUTIVE points, so what has to fit is base + count - 1.
|
||||
EXPECT_FALSE(violation("layout(binding = 32) buffer Buffer { int x; } g_array[4];\n").has_value());
|
||||
EXPECT_TRUE(violation("layout(binding = 34) buffer Buffer { int x; } g_array[4];\n").has_value());
|
||||
|
||||
// Qualifiers and a second layout list may sit between the binding and the keyword.
|
||||
EXPECT_TRUE(violation("layout(std430) layout(binding = 36) coherent restrict buffer B { int x; };\n")
|
||||
.has_value());
|
||||
|
||||
// Things the scanner must NOT judge: a uniform block (a different ceiling), a storage block
|
||||
// with no explicit binding, the bare default-qualifier form, and an instance array whose size
|
||||
// is not a literal.
|
||||
EXPECT_FALSE(violation("layout(binding = 40) uniform Block { int x; };\n"
|
||||
"layout(binding = 0) buffer Buffer { int y; };\n")
|
||||
.has_value());
|
||||
EXPECT_FALSE(violation("buffer Buffer { int x; };\nconst int binding = 40;\n").has_value());
|
||||
EXPECT_FALSE(violation("layout(binding = 1) buffer;\nbuffer Buffer { int x; };\n").has_value());
|
||||
EXPECT_FALSE(violation("const int kCount = 4;\nlayout(binding = 34) buffer B { int x; } g[kCount];\n")
|
||||
.has_value());
|
||||
|
||||
// A backend that advertises no binding points has no ceiling to enforce.
|
||||
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value());
|
||||
}
|
||||
|
||||
// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style, so
|
||||
// layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The extractor used to accept
|
||||
// a base-10 digit run and nothing else: the hex spelling failed the test entirely and the
|
||||
// declaration silently lost its explicit location, while the octal one was read as decimal 10.
|
||||
// The identical defect sat on every array dimension and on layout(binding = N).
|
||||
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsReadsNonDecimalIntegerLiterals) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String source = R"(#version 430 core
|
||||
layout(location = 0xA) uniform vec4 hexLower;
|
||||
layout(location = 0X1f) uniform vec4 hexUpper;
|
||||
layout(location = 010) uniform vec4 octal;
|
||||
layout(location = 3u) uniform vec4 unsignedSuffix;
|
||||
layout(location = 0x2) uniform float hexArray[0x3];
|
||||
layout(location = 1.0) uniform vec4 notAnInteger;
|
||||
layout(location = 7f) uniform vec4 unknownSuffix;
|
||||
void main() {}
|
||||
)";
|
||||
|
||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
||||
ASSERT_EQ(locations.count("hexLower"), 1u);
|
||||
EXPECT_EQ(locations.at("hexLower"), 10);
|
||||
ASSERT_EQ(locations.count("hexUpper"), 1u);
|
||||
EXPECT_EQ(locations.at("hexUpper"), 31);
|
||||
ASSERT_EQ(locations.count("octal"), 1u);
|
||||
EXPECT_EQ(locations.at("octal"), 8) << "a leading zero is octal in GLSL, not decimal";
|
||||
ASSERT_EQ(locations.count("unsignedSuffix"), 1u);
|
||||
EXPECT_EQ(locations.at("unsignedSuffix"), 3);
|
||||
ASSERT_EQ(locations.count("hexArray"), 1u);
|
||||
EXPECT_EQ(locations.at("hexArray"), 2);
|
||||
|
||||
// Still never guessed at: a float and an unknown suffix are skipped, not rounded.
|
||||
EXPECT_EQ(locations.count("notAnInteger"), 0u);
|
||||
EXPECT_EQ(locations.count("unknownSuffix"), 0u);
|
||||
}
|
||||
|
||||
// A hexadecimal array dimension has to size the declarator's span too, or the declarator after it
|
||||
// in the same statement starts at the wrong location.
|
||||
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsSpansANonDecimalArrayDimension) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(
|
||||
"#version 430 core\nlayout(location = 50) uniform float first[0x3], second;\nvoid main() {}\n");
|
||||
ASSERT_EQ(locations.count("first"), 1u);
|
||||
EXPECT_EQ(locations.at("first"), 50);
|
||||
ASSERT_EQ(locations.count("second"), 1u);
|
||||
EXPECT_EQ(locations.at("second"), 53) << "0x3 is three elements, not zero and not three hundred";
|
||||
}
|
||||
|
||||
// KHR-GL43.explicit_uniform_location.uniform-loc-array-of-arrays: glslang reflects
|
||||
// `float u[2][3]` as "u[0][0]" and "u[1][0]", and the linker resolves such a name by stripping the
|
||||
// single trailing "[0]" - so the map has to answer "u[1]", not just "u". Without the pre-flattened
|
||||
// keys both records missed the map entirely and were first-fitted from location 0.
|
||||
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsExpandsArrayOfArraysElements) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String source = R"(#version 430 core
|
||||
layout(location = 2) uniform float two_d[2][3];
|
||||
layout(location = 20) uniform float three_d[2][2][4];
|
||||
layout(location = 40) uniform float one_d[3];
|
||||
void main() {}
|
||||
)";
|
||||
|
||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
||||
|
||||
// The root entry is unchanged - the synthesized keys are additional, never a replacement.
|
||||
ASSERT_EQ(locations.count("two_d"), 1u);
|
||||
EXPECT_EQ(locations.at("two_d"), 2);
|
||||
// One key per outer index, each starting a run of the innermost dimension (3 here).
|
||||
ASSERT_EQ(locations.count("two_d[0]"), 1u);
|
||||
EXPECT_EQ(locations.at("two_d[0]"), 2);
|
||||
ASSERT_EQ(locations.count("two_d[1]"), 1u);
|
||||
EXPECT_EQ(locations.at("two_d[1]"), 5);
|
||||
|
||||
// Three dimensions: glslang expands all but the innermost, so both outer indices are spelled.
|
||||
ASSERT_EQ(locations.count("three_d"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d"), 20);
|
||||
ASSERT_EQ(locations.count("three_d[0][0]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[0][0]"), 20);
|
||||
ASSERT_EQ(locations.count("three_d[0][1]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[0][1]"), 24);
|
||||
ASSERT_EQ(locations.count("three_d[1][0]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[1][0]"), 28);
|
||||
ASSERT_EQ(locations.count("three_d[1][1]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[1][1]"), 32);
|
||||
|
||||
// A 1-D array needs no expansion: stripping "[0]" already reaches the root.
|
||||
ASSERT_EQ(locations.count("one_d"), 1u);
|
||||
EXPECT_EQ(locations.at("one_d"), 40);
|
||||
EXPECT_EQ(locations.count("one_d[0]"), 0u);
|
||||
|
||||
// The declarator after an array-of-arrays still advances by the WHOLE element count.
|
||||
const UnorderedMap<String, Int> pair = ExtractExplicitUniformLocations(
|
||||
"#version 430 core\nlayout(location = 0) uniform float a[2][3], b;\nvoid main() {}\n");
|
||||
ASSERT_EQ(pair.count("b"), 1u);
|
||||
EXPECT_EQ(pair.at("b"), 6);
|
||||
}
|
||||
|
||||
// KHR-GL43.explicit_uniform_location: layout(binding = 0x2) on a sampler is the same literal defect
|
||||
// as the location one, and losing it costs the sampler its initial texture unit.
|
||||
TEST_F(ProgramUtilTest, ExtractExplicitOpaqueBindingsReadsNonDecimalIntegerLiterals) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String source = R"(#version 430 core
|
||||
layout(binding = 0x2) uniform sampler2D hexUnit;
|
||||
layout(binding = 012) uniform sampler2D octalUnit;
|
||||
layout(binding = 1u) uniform sampler2D suffixedUnit;
|
||||
void main() {}
|
||||
)";
|
||||
|
||||
const UnorderedMap<String, Uint> bindings = ExtractExplicitOpaqueBindings(source);
|
||||
ASSERT_EQ(bindings.count("hexUnit"), 1u);
|
||||
EXPECT_EQ(bindings.at("hexUnit"), 2u);
|
||||
ASSERT_EQ(bindings.count("octalUnit"), 1u);
|
||||
EXPECT_EQ(bindings.at("octalUnit"), 10u) << "012 is octal ten, not twelve";
|
||||
ASSERT_EQ(bindings.count("suffixedUnit"), 1u);
|
||||
EXPECT_EQ(bindings.at("suffixedUnit"), 1u);
|
||||
}
|
||||
|
||||
@@ -140,6 +140,29 @@ namespace {
|
||||
|
||||
void StubEndXfbPrimitivesQuery(MG_Backend::BackendQueryHandle) { ++g_stubXfbEndCount; }
|
||||
|
||||
// Stub backend occlusion queries. The host has no ES context, and BeginQuery refuses the
|
||||
// occlusion targets outright when the backend advertises no hook - so a conditional-render
|
||||
// test cannot get a legal predicate object without these. g_stubResultNs is the sample count
|
||||
// the "driver" reports, which is the whole input to the predicate.
|
||||
MG_Backend::BackendQueryHandle StubBeginOcclusionQuery() {
|
||||
return reinterpret_cast<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x54));
|
||||
}
|
||||
|
||||
void StubEndOcclusionQuery(MG_Backend::BackendQueryHandle) {}
|
||||
|
||||
void InstallStubBackendOcclusionQueries() {
|
||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||
backendGL.BeginOcclusionQuery = StubBeginOcclusionQuery;
|
||||
backendGL.EndOcclusionQuery = StubEndOcclusionQuery;
|
||||
backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable;
|
||||
backendGL.GetQueryResult64 = StubGetQueryResult64;
|
||||
backendGL.DeleteBackendQuery = StubDeleteBackendQuery;
|
||||
g_stubDeleteCount = 0;
|
||||
g_stubResultAvailable = true;
|
||||
g_stubResultObtainable = true;
|
||||
g_stubResultNs = 0;
|
||||
}
|
||||
|
||||
void InstallStubBackendXfbQueries() {
|
||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||
backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery;
|
||||
@@ -677,6 +700,108 @@ TEST_F(QueryTest, PrimitivesGeneratedKeepsTheBackendResultUnderTheCpuPreference)
|
||||
// unified truthy rule (set, non-empty, not "0", case-insensitive not "false").
|
||||
// Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises
|
||||
// the real end-to-end path rather than the struct field alone.
|
||||
// KHR-GL43.compute_shader.conditional-dispatching and the conditional_render family.
|
||||
// glBeginConditionalRender/glEndConditionalRender were bare stubs: every command inside a
|
||||
// conditional block executed whatever the query said, so the block that should have been
|
||||
// discarded ran and doubled the atomic counter the case reads back.
|
||||
TEST_F(QueryTest, ConditionalRenderResolvesItsPredicateFromTheOcclusionQuery) {
|
||||
ScopedBackendFunctionsOverride backendGuard;
|
||||
InstallStubBackendOcclusionQueries();
|
||||
|
||||
GLuint ids[2] = {0, 0};
|
||||
MG_Impl::GLImpl::GenQueries(2, ids);
|
||||
ASSERT_NE(ids[0], 0u);
|
||||
ASSERT_NE(ids[1], 0u);
|
||||
|
||||
// One span that saw samples and one that saw none, which is exactly the pair the
|
||||
// conformance case builds out of a passing and a failing depth test.
|
||||
g_stubResultNs = 1;
|
||||
MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]);
|
||||
MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
GLuint passedResult = 0xFFFFFFFFu;
|
||||
MG_Impl::GLImpl::GetQueryObjectuiv(ids[0], GL_QUERY_RESULT, &passedResult);
|
||||
ASSERT_EQ(passedResult, 1u);
|
||||
|
||||
g_stubResultNs = 0;
|
||||
MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[1]);
|
||||
MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// A block on the query that passed executes.
|
||||
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_TRUE(MG_State::pGLContext->IsConditionalRenderActive());
|
||||
EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||
MG_Impl::GLImpl::EndConditionalRender();
|
||||
EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive());
|
||||
EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||
|
||||
// A block on the query that did not passes nothing through.
|
||||
MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||
MG_Impl::GLImpl::EndConditionalRender();
|
||||
|
||||
// ...and the _INVERTED modes swap both verdicts.
|
||||
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT_INVERTED);
|
||||
EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||
MG_Impl::GLImpl::EndConditionalRender();
|
||||
MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_BY_REGION_NO_WAIT_INVERTED);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||
MG_Impl::GLImpl::EndConditionalRender();
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteQueries(2, ids);
|
||||
}
|
||||
|
||||
TEST_F(QueryTest, ConditionalRenderRejectsTheErrorsTheSpecNames) {
|
||||
ScopedBackendFunctionsOverride backendGuard;
|
||||
InstallStubBackendOcclusionQueries();
|
||||
|
||||
GLuint ids[2] = {0, 0};
|
||||
MG_Impl::GLImpl::GenQueries(2, ids);
|
||||
g_stubResultNs = 1;
|
||||
MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]);
|
||||
MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// GL 4.6 core 10.9, one rule at a time.
|
||||
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_TIME_ELAPSED);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM);
|
||||
EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive());
|
||||
|
||||
// A generated NAME is not yet a query object.
|
||||
MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::BeginConditionalRender(0, GL_QUERY_WAIT);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||
|
||||
// A query that is not an occlusion query cannot drive one.
|
||||
GLuint timerId = 0;
|
||||
MG_Impl::GLImpl::GenQueries(1, &timerId);
|
||||
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, timerId);
|
||||
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::BeginConditionalRender(timerId, GL_QUERY_WAIT);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
|
||||
// End without a block, and a nested Begin.
|
||||
MG_Impl::GLImpl::EndConditionalRender();
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
// The rejected nested Begin must not have disturbed the open block.
|
||||
EXPECT_EQ(MG_State::pGLContext->GetConditionalRenderQuery(), ids[0]);
|
||||
MG_Impl::GLImpl::EndConditionalRender();
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteQueries(2, ids);
|
||||
MG_Impl::GLImpl::DeleteQueries(1, &timerId);
|
||||
}
|
||||
|
||||
TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
|
||||
const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
Bool expected = false;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
@@ -365,6 +366,13 @@ TEST(DirectGLESSanity, RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance) {
|
||||
// MaxShaderStorageBufferBindings - 1 = 12, so a regression that stops reading the
|
||||
// probed cap and falls back to the struct default would surface as "binding = 7".
|
||||
caps.MaxShaderStorageBufferBindings = 13;
|
||||
// The indirect lowering reads its baseInstance through a storage block declared in the
|
||||
// VERTEX stage, which is optional in both APIs and which the GLESCapabilities default
|
||||
// (0, the spec minimum) therefore denies. This suite is pinning the shape of that
|
||||
// lowering, so it has to describe a driver that can actually have it - see
|
||||
// VertexStageStorageBlockUsable and the BaseInstanceInjectionGate suite for the
|
||||
// zero case.
|
||||
caps.MaxVertexShaderStorageBlocks = 1;
|
||||
|
||||
const MobileGL::String source = R"(#version 310 es
|
||||
highp int mg_BaseInstanceLowered;
|
||||
@@ -403,6 +411,9 @@ TEST(DirectGLESSanity, TheIndirectWordIndexIsOneBasedSoItsUnwrittenValueMeansNot
|
||||
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
||||
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
caps.MaxShaderStorageBufferBindings = 13;
|
||||
// See RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance: without a vertex-stage
|
||||
// storage block there is no word index to be one-based about.
|
||||
caps.MaxVertexShaderStorageBlocks = 1;
|
||||
|
||||
const MobileGL::String source = R"(#version 310 es
|
||||
highp int mg_BaseInstanceLowered;
|
||||
@@ -428,6 +439,10 @@ TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) {
|
||||
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
||||
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
caps.MaxShaderStorageBufferBindings = 13;
|
||||
// Set explicitly even though the assertions below would also hold on the degraded path:
|
||||
// this case is about a CONFORMING driver leaving gl_InstanceID alone, and it would be a
|
||||
// silent weakening for it to be exercising the no-storage-block fallback instead.
|
||||
caps.MaxVertexShaderStorageBlocks = 1;
|
||||
|
||||
const MobileGL::String source = R"(#version 310 es
|
||||
highp int mg_BaseInstanceLowered;
|
||||
@@ -442,6 +457,8 @@ void main() {
|
||||
|
||||
EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos);
|
||||
EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos);
|
||||
// The indirect view is present on this driver, so the fallback must NOT have fired.
|
||||
EXPECT_NE(rewritten.find("buffer mg_IndirectParams"), MobileGL::String::npos);
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) {
|
||||
@@ -913,6 +930,161 @@ void main() {
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
}
|
||||
|
||||
// KHR-GL43.shader_atomic_counters.basic-glsl-built-in, .basic-buffer-bind and .basic-api-get.
|
||||
// The atomic-counter limits used to live in two unreconciled tables - glslang compiled every
|
||||
// shader against ONE binding while glGetIntegerv advertised thirty-six - and three of the enums
|
||||
// had no case in the getter at all, so the query raised INVALID_ENUM and left the caller reading
|
||||
// whatever was in its own stack slot.
|
||||
TEST(GetterSanity, AtomicCounterQueriesMatchShaderCompilerLimits) {
|
||||
using namespace MobileGL;
|
||||
namespace Transpiler = MG_Util::ShaderTranspiler;
|
||||
|
||||
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{});
|
||||
|
||||
GLint reported = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &reported);
|
||||
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, &reported);
|
||||
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE));
|
||||
for (const GLenum pname : {GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS, GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS,
|
||||
GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS}) {
|
||||
reported = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(pname, &reported);
|
||||
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE))
|
||||
<< "pname " << pname;
|
||||
}
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// glBindBufferBase sets the GENERIC binding point too (GL 4.6 6.1.1), and this is the one
|
||||
// indexed-buffer family whose non-indexed query had no case.
|
||||
reported = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_ATOMIC_COUNTER_BUFFER_BINDING, &reported);
|
||||
EXPECT_EQ(reported, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLuint buffer = 0;
|
||||
MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MG_Impl::GLImpl::BindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
|
||||
MG_Impl::GLImpl::BufferData(GL_ATOMIC_COUNTER_BUFFER, 64, nullptr, GL_STATIC_DRAW);
|
||||
MG_Impl::GLImpl::BindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 2, buffer);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_ATOMIC_COUNTER_BUFFER_BINDING, &reported);
|
||||
EXPECT_EQ(static_cast<GLuint>(reported), buffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// The advertised ceiling is also the one glBindBufferBase and the indexed getter enforce.
|
||||
// A limit nothing validates against is how these tables drifted apart in the first place:
|
||||
// the binding-point ARRAY is 36 deep, and it used to be that number an application saw.
|
||||
constexpr GLuint pastLastBinding = static_cast<GLuint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS);
|
||||
MG_Impl::GLImpl::BindBufferBase(GL_ATOMIC_COUNTER_BUFFER, pastLastBinding, buffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_ATOMIC_COUNTER_BUFFER_BINDING, pastLastBinding, &reported);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
|
||||
|
||||
// ...and the shading language has to expand the same numbers. Each array is sized by a
|
||||
// built-in constant and indexed at its last element with a literal, so the stage only
|
||||
// compiles when that constant is at least what glGetIntegerv just reported - which it was
|
||||
// not while the resource table said one.
|
||||
const String lastBinding = std::to_string(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS - 1);
|
||||
const String lastBuffer = std::to_string(Transpiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE - 1);
|
||||
const String source = R"(#version 430 core
|
||||
out vec4 color;
|
||||
int mgBindings[gl_MaxAtomicCounterBindings];
|
||||
int mgCombinedBuffers[gl_MaxCombinedAtomicCounterBuffers];
|
||||
int mgFragmentBuffers[gl_MaxFragmentAtomicCounterBuffers];
|
||||
layout(binding = )" + lastBinding + R"(, offset = 0) uniform atomic_uint mgCounter;
|
||||
void main() {
|
||||
color = vec4(float(mgBindings[)" + lastBinding + R"(] + mgCombinedBuffers[)" + lastBuffer +
|
||||
R"(] + mgFragmentBuffers[)" + lastBuffer + R"(] + int(atomicCounterIncrement(mgCounter))));
|
||||
}
|
||||
)";
|
||||
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||
.shaderType = GL_FRAGMENT_SHADER,
|
||||
.sourceStr = source,
|
||||
});
|
||||
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// KHR-GL43.compute_shader.max: the test queries every GL_MAX_COMPUTE_* value through the API and
|
||||
// then makes a compute shader compare the matching gl_MaxCompute* constant against it. The two
|
||||
// used to be independent tables and gl_MaxComputeWorkGroupSize.z disagreed - glslang compiled
|
||||
// against a permissive 1024 while the context advertises the 64 the GL 4.6 minimum (and every ES
|
||||
// driver) reports.
|
||||
TEST(GetterSanity, ComputeWorkGroupQueriesMatchShaderCompilerLimits) {
|
||||
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{});
|
||||
|
||||
GLint size[3] = {0, 0, 0};
|
||||
GLint count[3] = {0, 0, 0};
|
||||
for (GLuint index = 0; index < 3; ++index) {
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, &size[index]);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index, &count[index]);
|
||||
}
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// The compile runs against a captured env, exactly as the pipeline's does. That is the whole
|
||||
// invariant: the env holds the same floored driver answer GetIntegeri_v just returned, so the
|
||||
// resource table and the query agree BY CONSTRUCTION rather than by two tables happening to
|
||||
// carry the same literals.
|
||||
const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv();
|
||||
for (GLuint index = 0; index < 3; ++index) {
|
||||
EXPECT_EQ(static_cast<GLint>(env->maxComputeWorkGroupSize[index]), size[index]) << "index " << index;
|
||||
EXPECT_EQ(static_cast<GLint>(env->maxComputeWorkGroupCount[index]), count[index]) << "index " << index;
|
||||
}
|
||||
|
||||
// A negative array size is a compile error, so the stage only compiles when EVERY component
|
||||
// of both built-in constants equals what the query above reported. Two-sided by construction:
|
||||
// a resource table that is too permissive fails it exactly like one that is too tight.
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
const int mgAgree = (gl_MaxComputeWorkGroupSize == ivec3()" +
|
||||
std::to_string(size[0]) + ", " + std::to_string(size[1]) + ", " +
|
||||
std::to_string(size[2]) + R"() &&
|
||||
gl_MaxComputeWorkGroupCount == ivec3()" +
|
||||
std::to_string(count[0]) + ", " + std::to_string(count[1]) + ", " +
|
||||
std::to_string(count[2]) + R"()) ? 1 : -1;
|
||||
int mgProbe[mgAgree];
|
||||
void main() {
|
||||
mgProbe[0] = 0;
|
||||
}
|
||||
)";
|
||||
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||
.shaderType = GL_COMPUTE_SHADER,
|
||||
.sourceStr = source,
|
||||
.env = env.get(),
|
||||
});
|
||||
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
|
||||
|
||||
// The z ceiling is also what glslang checks a declared local_size_z against, so it has to
|
||||
// reject one invocation past the advertised limit and accept the limit itself.
|
||||
const String atLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2]) +
|
||||
") in;\nvoid main() {}\n";
|
||||
const String pastLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2] + 1) +
|
||||
") in;\nvoid main() {}\n";
|
||||
EXPECT_TRUE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||
.shaderType = GL_COMPUTE_SHADER,
|
||||
.sourceStr = atLimit,
|
||||
.env = env.get(),
|
||||
}));
|
||||
EXPECT_FALSE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||
.shaderType = GL_COMPUTE_SHADER,
|
||||
.sourceStr = pastLimit,
|
||||
.env = env.get(),
|
||||
}));
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
|
||||
using namespace MobileGL;
|
||||
|
||||
|
||||
@@ -544,14 +544,16 @@ namespace {
|
||||
// (2) the advertised extension vector, including the fp64 flag's own extension
|
||||
a.advertisedExtensions = {E_GL_ARB_gpu_shader_fp64, E_GL_KHR_debug};
|
||||
b.advertisedExtensions = {};
|
||||
// (3) the compute limits (ValidateComputeLocalSizeLimits only)
|
||||
a.maxComputeWorkGroupSize[0] = 1024;
|
||||
a.maxComputeWorkGroupSize[1] = 1024;
|
||||
a.maxComputeWorkGroupSize[2] = 64;
|
||||
// (3) the compute INVOCATION limit, and deliberately not the work-group size or
|
||||
// count any more. Those two used to sit here on the grounds that
|
||||
// ValidateComputeLocalSizeLimits was their only consumer; wave3 (cb155c5b) made
|
||||
// BuildTBuiltInResource read them, and glslang expands both into built-in constants
|
||||
// (gl_MaxComputeWorkGroupSize / gl_MaxComputeWorkGroupCount), so they are now
|
||||
// front-end inputs and belong in TheFrontendFingerprintMovesWithEveryFrontendLimit
|
||||
// instead - which is where they moved. The invocation limit is the one that really
|
||||
// still stops at the pre-parse gate: glslang has no built-in constant for it and
|
||||
// BuildTBuiltInResource does not read it.
|
||||
a.maxComputeWorkGroupInvocations = 128;
|
||||
b.maxComputeWorkGroupSize[0] = 2048;
|
||||
b.maxComputeWorkGroupSize[1] = 2048;
|
||||
b.maxComputeWorkGroupSize[2] = 1024;
|
||||
b.maxComputeWorkGroupInvocations = 2048;
|
||||
// (4) a spread of DynamicBackendParameters fields the front end never reads
|
||||
a.params.MaxColorTextureSamples = 1;
|
||||
@@ -598,8 +600,11 @@ TEST_F(TranslationCacheTest, TwoBackendsCompilingTheSameGlslShareOneL1Entry) {
|
||||
}
|
||||
|
||||
// The other direction, one case per input that was KEPT. Each is a limit the front end
|
||||
// really consumes - the seven BuildTBuiltInResource copies into TBuiltInResource, plus the
|
||||
// really consumes - everything BuildTBuiltInResource copies into TBuiltInResource, plus the
|
||||
// two inputs to the reflection vertex-attrib limit - so each must still split the key.
|
||||
// KEEP THIS LIST IN STEP WITH BuildTBuiltInResource: a limit that becomes env-derived there
|
||||
// and is not added here is a silent miscompile with no failing test to catch it, which is
|
||||
// precisely how the compute work-group cases below arrived.
|
||||
TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit) {
|
||||
const CompileEnv base;
|
||||
const Uint64 baseline = ComputeFrontendCompileEnvFingerprint(base);
|
||||
@@ -613,6 +618,21 @@ TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit)
|
||||
{"params.MaxComputeImageUniforms", [](CompileEnv& e) { e.params.MaxComputeImageUniforms += 1; }},
|
||||
{"params.MaxCombinedImageUniforms", [](CompileEnv& e) { e.params.MaxCombinedImageUniforms += 1; }},
|
||||
{"params.MaxVertexAttribs", [](CompileEnv& e) { e.params.MaxVertexAttribs += 1; }},
|
||||
// Env-derived since wave3's cb155c5b: BuildTBuiltInResource copies all seven of
|
||||
// these into TBuiltInResource, and glslang expands each into a built-in constant a
|
||||
// compute shader can read (gl_MaxComputeTextureImageUnits,
|
||||
// gl_MaxComputeWorkGroupSize, gl_MaxComputeWorkGroupCount). A module that reads one
|
||||
// compiles to different SPIR-V under two different values, so each must split the
|
||||
// key - one case per COMPONENT, because a per-axis difference is exactly the shape
|
||||
// real drivers produce (z = 64 on ES against 1024 elsewhere).
|
||||
{"params.MaxComputeTextureImageUnits",
|
||||
[](CompileEnv& e) { e.params.MaxComputeTextureImageUnits += 1; }},
|
||||
{"maxComputeWorkGroupSize[0]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[0] += 1; }},
|
||||
{"maxComputeWorkGroupSize[1]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[1] += 1; }},
|
||||
{"maxComputeWorkGroupSize[2]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[2] += 1; }},
|
||||
{"maxComputeWorkGroupCount[0]", [](CompileEnv& e) { e.maxComputeWorkGroupCount[0] += 1; }},
|
||||
{"maxComputeWorkGroupCount[1]", [](CompileEnv& e) { e.maxComputeWorkGroupCount[1] += 1; }},
|
||||
{"maxComputeWorkGroupCount[2]", [](CompileEnv& e) { e.maxComputeWorkGroupCount[2] += 1; }},
|
||||
// HasBackend(): with no backend the reflection attrib limit falls back to the
|
||||
// storage capacity rather than the driver's number, so the bit is load-bearing.
|
||||
{"HasBackend", [](CompileEnv& e) { e.backend = BackendType::DirectGLES; }},
|
||||
@@ -704,6 +724,44 @@ TEST_F(TranslationCacheTest, AProgramServedFromTheMemoAnswersTheWholeQuerySurfac
|
||||
}
|
||||
}
|
||||
|
||||
// glGetFragDataLocation on a program served from the memo.
|
||||
//
|
||||
// Split out from the case above because it caught a REAL bug that case did not: every
|
||||
// accessor it checks had already been moved onto the owned reflection snapshot, but
|
||||
// GetFragmentDataLocation still opened with `if (!Artifacts().program) return -1` and then
|
||||
// walked the live TProgram's pipe outputs. On a hit there is no TProgram - that is the whole
|
||||
// point of the memo - so the guard fired and the function reported "this program has no such
|
||||
// fragment output" for an output that plainly exists. The failure mode was silent and
|
||||
// asymmetric: the FIRST program with a given source answered correctly and every later one
|
||||
// answered -1, so nothing that linked a program once could see it.
|
||||
//
|
||||
// Both the explicit-request path (glBindFragDataLocation, answered from
|
||||
// linkedFragDataLocation) and the shader-declared path (layout(location = 0), answered from
|
||||
// the pipe-output snapshot) are checked, because only the second one reads the field that
|
||||
// used to come off the TProgram.
|
||||
TEST_F(TranslationCacheTest, AProgramServedFromTheMemoStillAnswersGetFragDataLocation) {
|
||||
const SyncCompileScope sync;
|
||||
const CacheModeScope cacheOn(true);
|
||||
const String fs = SwizzleLikeFragment("");
|
||||
|
||||
const GLuint parsed = LinkProgramFromSources(kVertexSource, fs);
|
||||
const TranslationCacheStats afterFirst = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||
const GLuint fromMemo = LinkProgramFromSources(kVertexSource, fs);
|
||||
const TranslationCacheStats afterSecond = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||
ASSERT_EQ(afterSecond.hits - afterFirst.hits, 1u) << "the second link was not a hit";
|
||||
|
||||
const Int parsedLocation = MG_Impl::GLImpl::GetFragDataLocation(parsed, "fragColor");
|
||||
const Int memoLocation = MG_Impl::GLImpl::GetFragDataLocation(fromMemo, "fragColor");
|
||||
EXPECT_EQ(parsedLocation, 0) << "the parsed program's own answer moved; this case is testing "
|
||||
"the wrong thing";
|
||||
EXPECT_EQ(memoLocation, parsedLocation)
|
||||
<< "a program served from the L1 memo lost its fragment output location";
|
||||
|
||||
// A name that is not an output must still be -1 from both, so the case cannot pass by
|
||||
// making the accessor answer everything.
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetFragDataLocation(fromMemo, "notAnOutput"), -1);
|
||||
}
|
||||
|
||||
// The modules a hit hands out must be the modules a from-scratch translation would have
|
||||
// produced. Without this the case above would still pass if the cache returned garbage.
|
||||
TEST_F(TranslationCacheTest, L1HitsAgreeWithACacheDisabledTranslation) {
|
||||
|
||||
@@ -377,6 +377,40 @@ TEST_F(TextureTest, ClearTexImageErrorContracts) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_ENUM));
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear entry points.
|
||||
// The generic GL_COMPRESSED_* enums are the half that needs its own tag - MobileGL answers them
|
||||
// with uncompressed storage on purpose, so by the time the clear runs the level looks like any
|
||||
// other RGBA8 image unless the REQUEST was recorded alongside it.
|
||||
TEST_F(TextureTest, ClearTexImageRejectsCompressedTextures) {
|
||||
GLuint genericTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &genericTexture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, genericTexture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::ClearTexImage(genericTexture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
MG_Impl::GLImpl::ClearTexSubImage(genericTexture, 0, 0, 0, 0, 4, 4, 1, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// A specific compressed internalformat is refused through the tag the level already carried...
|
||||
GLuint specificTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &specificTexture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, specificTexture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// ...and respecifying the level with an uncompressed format makes it clearable again, because
|
||||
// AllocateStorage clears both tags.
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R8, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv
|
||||
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
|
||||
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
|
||||
@@ -1023,6 +1057,32 @@ TEST_F(TextureTest, TexImage2DAcceptsSpecCompliantFormatCombinations) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL_STENCIL_INDEX is the unsized base format for stencil-only storage, and refusing it as an
|
||||
// internal format killed the ARB_clear_texture stencil case in its own setup - before it could
|
||||
// reach the calls it actually tests. The stencil-only transfer format stays paired with
|
||||
// stencil-only storage in both directions, which is what keeps those clears erroring.
|
||||
TEST_F(TextureTest, StencilIndexIsATextureInternalFormatPairedOnlyWithStencilStorage) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_STENCIL_INDEX, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
ASSERT_NE(textureObject, nullptr);
|
||||
EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::StencilIndex8);
|
||||
|
||||
// A colour transfer format against stencil storage is still INVALID_OPERATION, so the clear
|
||||
// the conformance case makes next fails the way it is supposed to.
|
||||
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// ...and the other direction: GL_STENCIL_INDEX against colour storage stays illegal.
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// Desktop GL table 3.3 lists GREEN and BLUE as TexImage client formats (GL CTS packed_pixels
|
||||
// rgba8_format_green/blue upload with them and verify the readback): the single input component
|
||||
// feeds the named channel, the other color channels default to 0 and alpha to 1.
|
||||
@@ -1317,6 +1377,56 @@ TEST_F(TextureTest, GetTextureImageReadsNamedObjectWithoutBinding) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.11.4 asks a readback for cube completeness and nothing else, so a mip chain whose
|
||||
// levels BELOW the requested one were never defined is still readable at that level - which is
|
||||
// exactly the shape ARB_clear_texture's conformance cases build (they define only the level they
|
||||
// clear). The whole-chain completeness gate used to answer INVALID_OPERATION here.
|
||||
TEST_F(TextureTest, GetTexImageReadsALevelWhoseLowerLevelsWereNeverDefined) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
const Uint8 pixels[] = {
|
||||
61, 62, 63, 64,
|
||||
71, 72, 73, 74,
|
||||
};
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
Uint8 output[sizeof(pixels)] = {};
|
||||
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 2, GL_RGBA, GL_UNSIGNED_BYTE, output);
|
||||
|
||||
EXPECT_EQ(std::memcmp(output, pixels, sizeof(pixels)), 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The other half of the same rule: loosening the chain-wide check must not let a level that holds
|
||||
// no image at all through. Level 0 exists as a chain slot once level 2 is defined, but nothing ever
|
||||
// gave it an image, so it stays INVALID_OPERATION - as does a level past the end of the chain and a
|
||||
// texture that was never given any image whatsoever.
|
||||
TEST_F(TextureTest, GetTexImageStillRejectsALevelThatHoldsNoImage) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
Uint8 output[4] = {};
|
||||
|
||||
// No image at all yet: the chain carries no levels.
|
||||
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Inside the chain, but never defined.
|
||||
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// Past the end of the chain.
|
||||
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 3, GL_RGBA, GL_UNSIGNED_BYTE, output);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, GetTextureSubImageReadsFullNamedLevelWithoutBinding) {
|
||||
GLuint texture = 0;
|
||||
GLuint boundTexture = 0;
|
||||
@@ -1651,6 +1761,56 @@ TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same rule for the 3D entry points, which never recorded the tag at all. Besides the two
|
||||
// level queries this decides the level's texel BLOCK SIZE, which glCopyImageSubData compares
|
||||
// against the other endpoint's - an untagged GL_COMPRESSED_RG_RGTC2 array level measured as the
|
||||
// RG8 storage it resolves to, 2 bytes instead of 16.
|
||||
TEST_F(TextureTest, TexImage3DAndTexStorage3DTagASpecificCompressedInternalFormat) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 2, 0, GL_RG,
|
||||
GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint compressed = GL_FALSE;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed);
|
||||
EXPECT_EQ(compressed, GL_TRUE);
|
||||
|
||||
GLint internalFormat = 0;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
|
||||
EXPECT_EQ(internalFormat, static_cast<GLint>(GL_COMPRESSED_RG_RGTC2));
|
||||
|
||||
// 8x8 in 4x4 blocks of 16 bytes each is 64 bytes a layer, and both layers count.
|
||||
GLint imageSize = 0;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
|
||||
EXPECT_EQ(imageSize, 128);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// The texel shadow behind the tag keeps the uncompressed storage the format resolves to.
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
ASSERT_NE(textureObject, nullptr);
|
||||
EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::RG8);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
// glTexStorage3D has the same gap and the same fix; immutable storage plus
|
||||
// glCompressedTexSubImage3D is the modern way to upload a compressed array texture.
|
||||
GLuint storageTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &storageTexture);
|
||||
MG_Impl::GLImpl::TextureStorage3D(storageTexture, 1, GL_COMPRESSED_RG_RGTC2, 8, 8, 2);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, storageTexture);
|
||||
|
||||
compressed = GL_FALSE;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed);
|
||||
EXPECT_EQ(compressed, GL_TRUE);
|
||||
imageSize = 0;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
|
||||
EXPECT_EQ(imageSize, 128);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way
|
||||
// KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per
|
||||
@@ -3320,6 +3480,29 @@ TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts)
|
||||
TexturePixelDataType::UnsignedInt5999Rev));
|
||||
}
|
||||
|
||||
TEST(SharedExponentRGB9E5Test, RedundantPackedEncodingIsRGB9E5Only) {
|
||||
using MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding;
|
||||
|
||||
// This is the predicate that decides whether the CPU shadow has to answer glGetTexImage
|
||||
// instead of a GPU readback, so it must be as narrow as the defect: only the shared exponent
|
||||
// has several legal encodings of one value.
|
||||
EXPECT_TRUE(HasRedundantPackedEncoding(TextureInternalFormat::RGB9E5));
|
||||
|
||||
// The other three packed 32-bit layouts round-trip through float32 bit-exactly (each field is
|
||||
// either an integer or a unique float encoding), so a GPU readback still serves them - which
|
||||
// matters because RGB10_A2 and R11F_G11F_B10F ARE colour-renderable and their shadow can
|
||||
// legitimately be stale.
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2UI));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::R11FG11FB10F));
|
||||
|
||||
// Nothing unpacked qualifies, and neither does an unknown format.
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA8));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA32F));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB8));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::Unknown));
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) {
|
||||
// Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client
|
||||
// words untouched, including the redundant encodings the CTS generates.
|
||||
@@ -4011,24 +4194,30 @@ TEST_F(TextureTest, CopyTexImage1DReportsUnsupportedInsteadOfTerminating) {
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerminating) {
|
||||
// TextureStorageType is {Mipmap, Buffer} and the level queries only answer out of a mipmap
|
||||
// chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a
|
||||
// THROW_UNIMPL_EXCEPTION default: label and killed the process.
|
||||
TEST_F(TextureTest, GetTexLevelParameterAnswersBufferStorageGeometry) {
|
||||
// TextureStorageType is {Mipmap, Buffer} and the level queries used to answer only out of a
|
||||
// mipmap chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a
|
||||
// THROW_UNIMPL_EXCEPTION default: label and killed the process. It now answers out of the
|
||||
// attached buffer range instead (GL 4.6 core 8.9): a buffer texture is one-dimensional, and
|
||||
// with no buffer attached it addresses no texels at all.
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
for (const GLenum pname : {GL_TEXTURE_WIDTH, GL_TEXTURE_HEIGHT, GL_TEXTURE_DEPTH}) {
|
||||
const std::pair<GLenum, GLint> expectations[] = {
|
||||
{GL_TEXTURE_WIDTH, 0}, {GL_TEXTURE_HEIGHT, 1}, {GL_TEXTURE_DEPTH, 1}};
|
||||
for (const auto& [pname, expected] : expectations) {
|
||||
GLint intParam = 0x20202020;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &intParam);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(intParam, expected) << "pname " << pname;
|
||||
|
||||
GLfloat floatParam = 12345.0f;
|
||||
MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &floatParam);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(floatParam, static_cast<GLfloat>(expected)) << "pname " << pname;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4100,24 +4289,25 @@ namespace {
|
||||
GLint SrcZ = -1;
|
||||
GLint DstZ = -1;
|
||||
GLsizei Depth = -1;
|
||||
Bool SrcIsRenderbuffer = false;
|
||||
Bool DstIsRenderbuffer = false;
|
||||
} g_copyImageSubDataCall;
|
||||
|
||||
void RecordCopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, GLenum srcTarget,
|
||||
void RecordCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget,
|
||||
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, GLenum dstTarget,
|
||||
const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget,
|
||||
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
|
||||
GLsizei srcHeight, GLsizei srcDepth) {
|
||||
(void)srcTexture;
|
||||
(void)srcLevel;
|
||||
(void)srcX;
|
||||
(void)srcY;
|
||||
(void)dstTexture;
|
||||
(void)dstLevel;
|
||||
(void)dstX;
|
||||
(void)dstY;
|
||||
(void)srcWidth;
|
||||
(void)srcHeight;
|
||||
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth};
|
||||
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ,
|
||||
dstZ, srcDepth, src.IsRenderbuffer(), dst.IsRenderbuffer()};
|
||||
}
|
||||
|
||||
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
|
||||
@@ -4354,9 +4544,13 @@ TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) {
|
||||
MG_Impl::GLImpl::GenTextures(1, &reusedSrc);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedSrc);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
MG_Impl::GLImpl::GenTextures(1, &reusedDst);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedDst);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1,
|
||||
@@ -4388,3 +4582,274 @@ TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated)
|
||||
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER as an endpoint target, and a renderbuffer name lives
|
||||
// in its own namespace. Resolving BOTH names through the texture namespace answered a null object
|
||||
// for every renderbuffer endpoint, so all 74 conformance cases that name one - the whole
|
||||
// texture<->renderbuffer half of KHR-GL43.copy_image, plus its smoke test - reported
|
||||
// GL_INVALID_VALUE. The endpoint is a sum type now; the target picks the namespace.
|
||||
TEST_F(TextureTest, CopyImageSubDataResolvesARenderbufferEndpointInTheRenderbufferNamespace) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8);
|
||||
GLuint renderbuffer = 0;
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
|
||||
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.SrcIsRenderbuffer);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_RENDERBUFFER));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// ...and back the other way, which is the second half of the conformance case's two-copy
|
||||
// shape (texture -> renderbuffer -> texture).
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.DstIsRenderbuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Renderbuffer to renderbuffer, the shape neither endpoint could take before, plus the negative
|
||||
// that pins which table was consulted: with GL_RENDERBUFFER named, a number that is not a live
|
||||
// RENDERBUFFER is INVALID_VALUE - the texture table is never asked.
|
||||
TEST_F(TextureTest, CopyImageSubDataKeepsTheTwoNameNamespacesApart) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcRenderbuffer = 0;
|
||||
GLuint dstRenderbuffer = 0;
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &srcRenderbuffer);
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &dstRenderbuffer);
|
||||
MG_Impl::GLImpl::NamedRenderbufferStorage(srcRenderbuffer, GL_RGBA8, 8, 8);
|
||||
MG_Impl::GLImpl::NamedRenderbufferStorage(dstRenderbuffer, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, dstRenderbuffer,
|
||||
GL_RENDERBUFFER, 0, 0, 0, 0, 4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, 4243, GL_RENDERBUFFER, 0, 0, 0,
|
||||
0, 4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// A renderbuffer has exactly one image, so any level above zero is the same INVALID_VALUE a
|
||||
// texture gets for a level it does not have - and an unallocated one is an incomplete image,
|
||||
// which 18.3.2 spells INVALID_OPERATION.
|
||||
TEST_F(TextureTest, CopyImageSubDataChecksARenderbufferLevelAndStorage) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8);
|
||||
GLuint renderbuffer = 0;
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
|
||||
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 1, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
GLuint emptyRenderbuffer = 0;
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &emptyRenderbuffer);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, emptyRenderbuffer, GL_RENDERBUFFER, 0, 0,
|
||||
0, 0, 4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// A 16-byte RGTC2 block and a 16-byte RGBA32UI texel are in the same size class, so GL 4.6 core
|
||||
// 18.3.2 requires this copy to succeed. It did not for an ARRAY source: glTexImage3D recorded no
|
||||
// specific-compressed-format tag, so the level was measured as the 2-byte RG8 storage RGTC2
|
||||
// resolves to and the compatibility rule saw 2 against 16.
|
||||
TEST_F(TextureTest, CopyImageSubDataSizesACompressedArrayLevelByItsBlock) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint compressedSource = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &compressedSource);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, compressedSource);
|
||||
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 1, 0, GL_RG,
|
||||
GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
GLuint uncompressedDestination = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &uncompressedDestination);
|
||||
MG_Impl::GLImpl::TextureStorage3D(uncompressedDestination, 1, GL_RGBA32UI, 8, 8, 1);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(compressedSource, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, uncompressedDestination,
|
||||
GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 8, 8, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// 18.3.2 requires INVALID_OPERATION when either object is an INCOMPLETE TEXTURE, and completeness
|
||||
// is GL 4.6 core 8.17's - which includes the mip chain whenever the minification filter reads it.
|
||||
// A mutable texture with level 0 alone still carries the default NEAREST_MIPMAP_LINEAR filter, so
|
||||
// it is mipmap incomplete; the storage-only IsComplete() this used to ask called it complete and
|
||||
// let the copy through, which is the whole of KHR-GL43.copy_image.incomplete_tex.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsAMipmapIncompleteTexture) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint incomplete = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &incomplete);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
GLuint complete = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &complete);
|
||||
MG_Impl::GLImpl::TextureStorage2D(complete, 1, GL_RGBA8, 16, 16);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
|
||||
4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// The destination side is checked the same way.
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(complete, GL_TEXTURE_2D, 0, 0, 0, 0, incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
|
||||
4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// Capping TEXTURE_MAX_LEVEL at the one level that exists is what the conformance suite's
|
||||
// makeTextureComplete does, and it is enough to make the same object complete.
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
|
||||
4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The targets that have no mip chain must not be dragged in: GL 4.6 core 8.17 makes q equal to
|
||||
// level_base for them, so no filter can make them mipmap incomplete. A rectangle texture gets a
|
||||
// non-mipmapping default filter from the object itself, so it would survive a predicate that
|
||||
// trusted the sampler alone - it is here because the whole texture path is one branch and this is
|
||||
// the cheap half of pinning it.
|
||||
TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToRectangleTextures) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcRectangle = 0;
|
||||
GLuint dstRectangle = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcRectangle);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstRectangle);
|
||||
MG_Impl::GLImpl::TextureStorage2D(srcRectangle, 1, GL_RGBA8, 8, 8);
|
||||
MG_Impl::GLImpl::TextureStorage2D(dstRectangle, 1, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcRectangle, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstRectangle,
|
||||
GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, 4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The multisample half, which is the one the target guard actually exists for: a multisample
|
||||
// texture keeps the shared NEAREST_MIPMAP_LINEAR default in its own sampler state (only the
|
||||
// rectangle constructor overrides it), so asking the mipmap predicate about it without the target
|
||||
// guard would report every 8x8 multisample image incomplete and refuse a legal copy.
|
||||
TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToMultisampleTextures) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcMultisample = 0;
|
||||
GLuint dstMultisample = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcMultisample);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstMultisample);
|
||||
MG_Impl::GLImpl::TextureStorage2DMultisample(srcMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE);
|
||||
MG_Impl::GLImpl::TextureStorage2DMultisample(dstMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
// This unit-test binary has no backend behind the renderable-format and sample-count queries,
|
||||
// so the storage may not have been created at all. Checked on the state objects rather than
|
||||
// assumed, so the case can only skip or test the real rule.
|
||||
const auto srcObject = MG_State::pGLContext->GetTextureObject(srcMultisample);
|
||||
const auto dstObject = MG_State::pGLContext->GetTextureObject(dstMultisample);
|
||||
ASSERT_NE(srcObject, nullptr);
|
||||
ASSERT_NE(dstObject, nullptr);
|
||||
if (!srcObject->IsComplete() || !dstObject->IsComplete()) {
|
||||
GTEST_SKIP() << "this context could not give the multisample textures storage";
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcMultisample, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstMultisample,
|
||||
GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.11 makes GL_IMAGE_FORMAT_COMPATIBILITY_TYPE readable through every
|
||||
// GetTexParameter form. Three of MobileGL's four getters answered it and glGetTexParameterfv did
|
||||
// not, so the float query raised GL_INVALID_ENUM and left the caller's float uninitialised
|
||||
// (KHR-GL4x.shader_image_load_store.basic-api-texParam reads it with both iv and fv and compares
|
||||
// them). Asserted across all four here, because an enum present in three of four parallel
|
||||
// switches is the drift shape that comes back.
|
||||
TEST_F(TextureTest, ImageFormatCompatibilityTypeAgreesAcrossEveryTexParameterGetter) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLint integerValue = 0;
|
||||
MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &integerValue);
|
||||
EXPECT_EQ(integerValue, GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLfloat floatValue = 0.0f;
|
||||
MG_Impl::GLImpl::GetTexParameterfv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &floatValue);
|
||||
EXPECT_FLOAT_EQ(floatValue, static_cast<GLfloat>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint signedValue = 0;
|
||||
MG_Impl::GLImpl::GetTexParameterIiv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &signedValue);
|
||||
EXPECT_EQ(signedValue, GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLuint unsignedValue = 0;
|
||||
MG_Impl::GLImpl::GetTexParameterIuiv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &unsignedValue);
|
||||
EXPECT_EQ(unsignedValue, static_cast<GLuint>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &texture);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user