Files
MobileGL/MobileGL/MG_Test/SanityTest.cpp
T
swung0x48 d40f753983 [Fix] (MG_State, MG_Impl, MG_Backend): conformant current generic vertex attribute values
GL 3.3 Core: a shader input whose generic attribute array is disabled reads that
attribute's current value (per-context state, default (0,0,0,1)). Four defects made
that path non-conformant, three of them silently.

* Out-of-bounds current-value reads. m_currentVertexAttributes held 16 entries while
  the DirectVulkan draw path walked shader input locations 0..31 and GL_MAX_VERTEX_ATTRIBS
  was advertised straight from the device (commonly 32). The only guard was MOBILEGL_ASSERT,
  which expands to nothing outside debug builds. Grow the storage capacity to 32, advertise
  min(device limit, capacity), validate against that dynamic limit, and give the accessors
  real runtime bounds checks. Replace the literal 32 loops with the constant, and pin
  MAX_VERTEX_ATTRIBS to the Uint32 mask width and to vertexInputTypes' bound with
  static_asserts so the two can no longer drift apart -- that drift was the bug.

* DirectGLES never fed current values to the driver. Values were stored in MG_State only,
  so a disabled attribute always rendered as the ES driver's own untouched (0,0,0,1) while
  DirectVulkan rendered it correctly: identical GL code, different pixels per backend.
  Add SyncCurrentVertexAttributeValues() to the draw prologue, and hoist the
  glType -> (base type, component count) dispatch into MG_State::GLState so both backends
  resolve the semantics from one place instead of it living inside VulkanRenderer.

* Enabled arrays the backend could not map were silently demoted to the current value.
  ToVkVertexFormat had no DataType::Float16 case, so a GL_HALF_FLOAT array fell to
  VK_FORMAT_UNDEFINED, dropped out of the vertex input state, and became indistinguishable
  from a disabled array: the geometry rendered a constant colour with GL_NO_ERROR. Add the
  Float16 mapping, track an unsupportedAttribMask, and hard-fail the draw before pipeline
  creation so no synthetic attribute is baked into a cached VkPipeline.

* glGetVertexAttrib{fv,iv,Iiv,Iuiv}(GL_CURRENT_VERTEX_ATTRIB) returned before any index
  validation, reading past the array instead of raising GL_INVALID_VALUE.

Also resolve ProgramObject::DoReflection's "TODO: get from backend" 16-location clamp,
which capped the new DirectGLES sync at locations 0..15; report GL_MAX_VERTEX_ATTRIBS
through the same helper the validators use, so the clamp cannot be bypassed; and bound
vertex binding indices by the same dynamic limit, since the default attribute -> binding
mapping is the identity.

Add a "Vertex attributes" driver POST row to both backends: FAIL below the GL 3.3 Core
minimum of 16, WARN above MobileGL's storage capacity (clamped, extra attributes unusable),
PASS in between -- making the driver/host mismatch that caused the out-of-bounds read
visible instead of silently swallowed.

Covered by 7 new regression tests (each verified to fail against the previous behaviour).
2026-07-10 11:23:16 -04:00

607 lines
28 KiB
C++

// MobileGL - MobileGL/MG_Test/SanityTest.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
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureState.h>
#include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VkTextureManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/Debug/Log.h>
namespace {
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
public:
explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params):
m_params(params) {}
void Initialize() override {}
MobileGL::Bool InitCapabilities() override { return true; }
MobileGL::Bool InitWindowSurface() override { return true; }
const MobileGL::RendererInfo& GetRendererInfo() const override { return m_info; }
MobileGL::String GetBackendAPIVersionString() const override { return "test"; }
const MobileGL::MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override {
return m_functions;
}
const MobileGL::MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override {
return m_params;
}
MobileGL::BackendType GetBackendType() const override { return MobileGL::BackendType::Unknown; }
private:
MobileGL::MG_Backend::DynamicBackendParameters m_params;
MobileGL::MG_Backend::GlobalBackendFunctionsTable m_functions{};
MobileGL::RendererInfo m_info{
.RendererName = "Test",
.BackendName = "DynamicParameterBackend",
.ExtraVendor = MobileGL::Nullopt,
.RendererGLInfo = {.TargetGLVersion = {3, 3, 0},
.TargetGLSLVersion = {4, 6, 0},
.Extensions = {},
.IsCompatibilityProfile = false},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
};
void SetEnvVar(const char* name, const char* value) {
#if defined(_WIN32)
_putenv_s(name, value);
#else
setenv(name, value, 1);
#endif
}
void UnsetEnvVar(const char* name) {
#if defined(_WIN32)
_putenv_s(name, "");
#else
unsetenv(name);
#endif
}
MobileGL::SizeT CountOccurrences(const MobileGL::String& haystack, const MobileGL::String& needle) {
MobileGL::SizeT count = 0;
for (MobileGL::SizeT pos = haystack.find(needle); pos != MobileGL::String::npos;
pos = haystack.find(needle, pos + needle.size())) {
++count;
}
return count;
}
// Snapshots the DirectGLES capability globals on construction and restores them on
// destruction, so tests that mutate g_GLESCapabilities cannot leak state into later
// tests even if an assertion or exception unwinds the test body early.
struct ScopedGLESCapabilitiesOverride {
ScopedGLESCapabilitiesOverride():
m_snapshot(MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities) {}
~ScopedGLESCapabilitiesOverride() {
MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities = m_snapshot;
}
ScopedGLESCapabilitiesOverride(const ScopedGLESCapabilitiesOverride&) = delete;
ScopedGLESCapabilitiesOverride& operator=(const ScopedGLESCapabilitiesOverride&) = delete;
private:
MobileGL::MG_External::GLESCapabilities m_snapshot;
};
} // namespace
TEST(Sanity, BasicAssertions) {
// Expect two strings not to be equal.
EXPECT_STRNE("hello", "world");
// Expect equality.
EXPECT_EQ(7 * 6, 42);
}
TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) {
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
const auto& extensions = backend.GetRendererInfo().RendererGLInfo.Extensions;
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end());
}
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
const auto& extensions = rendererInfo.Extensions;
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_shader_storage_buffer_object),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_texture_storage),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_direct_state_access),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_multi_draw_indirect),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_indirect_parameters),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_shader_draw_parameters),
extensions.end());
EXPECT_EQ(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_gpu_shader_int64),
extensions.end());
EXPECT_EQ(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_KHR_shader_subgroup),
extensions.end());
}
TEST(DirectGLESSanity, ProvidesNamedFramebufferBlitForDirectStateAccess) {
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
const auto& funcs = backend.GetBackendFunctions().GL;
EXPECT_NE(funcs.ClearNamedFramebufferfv, nullptr);
EXPECT_NE(funcs.ClearNamedFramebufferfi, nullptr);
EXPECT_NE(funcs.BlitFramebuffer, nullptr);
EXPECT_NE(funcs.BlitNamedFramebuffer, nullptr);
}
TEST(DirectGLESSanity, RewritesBaseInstanceBuiltinForEsslVertexShaders) {
const MobileGL::String source = R"(#version 320 es
void main() {
uint drawId = gl_BaseInstance;
uint untouched = my_gl_BaseInstance_value;
}
)";
const auto rewritten = MobileGL::MG_Backend::DirectGLES::EmulateBaseInstanceInVertexShader(
source, GL_VERTEX_SHADER);
EXPECT_NE(rewritten.find("uniform highp int mg_BaseInstance;"), MobileGL::String::npos);
EXPECT_NE(rewritten.find("uint drawId = mg_BaseInstance;"), MobileGL::String::npos);
EXPECT_NE(rewritten.find("my_gl_BaseInstance_value"), MobileGL::String::npos);
EXPECT_EQ(rewritten.find("uint drawId = gl_BaseInstance;"), MobileGL::String::npos);
}
TEST(DirectGLESSanity, LeavesBaseInstanceBuiltinAloneOutsideVertexShaders) {
const MobileGL::String source = "#version 320 es\nuint value = gl_BaseInstance;\n";
const auto rewritten = MobileGL::MG_Backend::DirectGLES::EmulateBaseInstanceInVertexShader(
source, GL_FRAGMENT_SHADER);
EXPECT_EQ(rewritten, source);
}
TEST(DirectGLESSanity, RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance) {
const ScopedGLESCapabilitiesOverride capsGuard;
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = true;
// Deliberately not the GLESCapabilities default (8): the injected block must land at
// 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;
const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered;
void main() {
int instance = gl_InstanceID + mg_BaseInstanceLowered;
gl_Position = vec4(float(instance));
}
)";
const auto rewritten = MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms(
source, GL_VERTEX_SHADER);
EXPECT_NE(rewritten.find("int instance = mg_ZeroBasedInstanceID + mg_BaseInstanceLowered;"),
MobileGL::String::npos);
EXPECT_NE(rewritten.find("#define mg_ZeroBasedInstanceID (gl_InstanceID - ((mg_BaseInstanceWordIndex >= 0) ? "
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex)]) : 0))"),
MobileGL::String::npos);
EXPECT_NE(rewritten.find(
"layout(std430, binding = 12) readonly buffer mg_IndirectParams { highp uint mg_indirectWords[]; };"),
MobileGL::String::npos);
// The one inside the #define machinery must be the only surviving gl_InstanceID.
EXPECT_EQ(CountOccurrences(rewritten, "gl_InstanceID"), 1u);
}
TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) {
const ScopedGLESCapabilitiesOverride capsGuard;
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13;
const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered;
void main() {
int instance = gl_InstanceID + mg_BaseInstanceLowered;
gl_Position = vec4(float(instance));
}
)";
const auto rewritten = MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms(
source, GL_VERTEX_SHADER);
EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos);
EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos);
}
TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) {
const ScopedGLESCapabilitiesOverride capsGuard;
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = true;
caps.MaxShaderStorageBufferBindings = 13;
const MobileGL::String source =
"#version 310 es\nhighp int mg_BaseInstanceLowered;\nint value = gl_InstanceID + mg_BaseInstanceLowered;\n";
const auto rewritten = MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms(
source, GL_FRAGMENT_SHADER);
EXPECT_EQ(rewritten, source);
}
TEST(DirectVulkanSanity, AdvertisesTextureStorageForDirectStateAccess) {
MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
const auto& extensions = backend.GetRendererInfo().RendererGLInfo.Extensions;
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_direct_state_access),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_texture_storage), extensions.end());
}
TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuffer) {
using MobileGL::MG_Backend::DirectVulkan::ResolveRenderPassFramebufferExtent;
const MobileGL::TextureSize attachmentExtent = {512, 512, 1};
const VkExtent2D swapchainExtent = {3200u, 1440u};
EXPECT_EQ(ResolveRenderPassFramebufferExtent(true, attachmentExtent, swapchainExtent),
MobileGL::IntVec2(3200, 1440));
EXPECT_EQ(ResolveRenderPassFramebufferExtent(false, attachmentExtent, swapchainExtent),
MobileGL::IntVec2(512, 512));
}
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
const auto& extensions = rendererInfo.Extensions;
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_shader_storage_buffer_object),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_multi_draw_indirect),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_indirect_parameters),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_shader_draw_parameters),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_gpu_shader_int64),
extensions.end());
}
TEST(DirectVulkanSanity, ClampsAdvertisedTextureAndDrawBufferLimitsToFrontendState) {
using namespace MobileGL;
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
MG_External::VulkanCapabilities highCaps;
highCaps.MaxTextureImageUnits = 256;
highCaps.MaxVertexTextureImageUnits = 256;
highCaps.MaxComputeTextureImageUnits = 256;
highCaps.MaxCombinedTextureImageUnits = 256;
highCaps.MaxDrawBuffers = 128;
highCaps.MaxColorAttachments = 128;
backend.ApplyVulkanCapabilitiesForTesting(highCaps);
const auto& highParams = backend.GetDynamicParameters();
EXPECT_EQ(highParams.MaxTextureImageUnits, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
EXPECT_EQ(highParams.MaxVertexTextureImageUnits, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
EXPECT_EQ(highParams.MaxComputeTextureImageUnits, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
EXPECT_EQ(highParams.MaxCombinedTextureImageUnits, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
EXPECT_EQ(highParams.MaxDrawBuffers, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
EXPECT_EQ(highParams.MaxColorAttachments, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
MG_External::VulkanCapabilities lowCaps;
lowCaps.MaxTextureImageUnits = 16;
lowCaps.MaxVertexTextureImageUnits = 12;
lowCaps.MaxComputeTextureImageUnits = 10;
lowCaps.MaxCombinedTextureImageUnits = 20;
lowCaps.MaxDrawBuffers = 4;
lowCaps.MaxColorAttachments = 6;
backend.ApplyVulkanCapabilitiesForTesting(lowCaps);
const auto& lowParams = backend.GetDynamicParameters();
EXPECT_EQ(lowParams.MaxTextureImageUnits, 16);
EXPECT_EQ(lowParams.MaxVertexTextureImageUnits, 12);
EXPECT_EQ(lowParams.MaxComputeTextureImageUnits, 10);
EXPECT_EQ(lowParams.MaxCombinedTextureImageUnits, 20);
EXPECT_EQ(lowParams.MaxDrawBuffers, 4);
EXPECT_EQ(lowParams.MaxColorAttachments, 6);
}
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
using namespace MobileGL;
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
MG_External::VulkanCapabilities unsupportedCaps;
unsupportedCaps.SupportsShaderSubgroup = false;
unsupportedCaps.SubgroupSize = 32;
unsupportedCaps.SubgroupSupportedStages = VK_SHADER_STAGE_COMPUTE_BIT;
unsupportedCaps.SubgroupSupportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT;
backend.ApplyVulkanCapabilitiesForTesting(unsupportedCaps);
const auto& unsupportedExtensions = backend.GetRendererInfo().RendererGLInfo.Extensions;
EXPECT_EQ(std::find(unsupportedExtensions.begin(), unsupportedExtensions.end(), E_GL_KHR_shader_subgroup),
unsupportedExtensions.end());
EXPECT_EQ(backend.GetDynamicParameters().SubgroupSize, 0u);
EXPECT_EQ(backend.GetDynamicParameters().SubgroupSupportedStages, 0u);
EXPECT_EQ(backend.GetDynamicParameters().SubgroupSupportedFeatures, 0u);
MG_External::VulkanCapabilities supportedCaps;
supportedCaps.SupportsShaderSubgroup = true;
supportedCaps.SubgroupSize = 32;
supportedCaps.SubgroupSupportedStages = VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT;
supportedCaps.SubgroupSupportedOperations =
VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_ARITHMETIC_BIT | VK_SUBGROUP_FEATURE_QUAD_BIT;
supportedCaps.SubgroupQuadOperationsInAllStages = true;
backend.ApplyVulkanCapabilitiesForTesting(supportedCaps);
const auto& supportedExtensions = backend.GetRendererInfo().RendererGLInfo.Extensions;
EXPECT_NE(std::find(supportedExtensions.begin(), supportedExtensions.end(), E_GL_KHR_shader_subgroup),
supportedExtensions.end());
EXPECT_EQ(backend.GetDynamicParameters().SubgroupSize, 32u);
EXPECT_EQ(backend.GetDynamicParameters().SubgroupSupportedStages,
static_cast<Uint32>(GL_FRAGMENT_SHADER_BIT | GL_COMPUTE_SHADER_BIT));
EXPECT_EQ(backend.GetDynamicParameters().SubgroupSupportedFeatures,
static_cast<Uint32>(GL_SUBGROUP_FEATURE_BASIC_BIT_KHR |
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR |
GL_SUBGROUP_FEATURE_QUAD_BIT_KHR));
EXPECT_TRUE(backend.GetDynamicParameters().SubgroupQuadOperationsInAllStages);
}
TEST(DirectVulkanSanity, KeepsOptionalGpuShaderInt64BranchForVoxyQuadDecode) {
using namespace MobileGL;
MG_Backend::pActiveBackendObject = MakeUnique<MG_Backend::DirectVulkan::BackendObject_DirectVulkan>();
String source = R"(#version 460 core
#extension GL_ARB_gpu_shader_int64 : enable
#ifdef GL_ARB_gpu_shader_int64
uint getLowBits(uint64_t v) {
return uint(v & uint64_t(0xffu));
}
#else
#error int64 branch should be enabled for DirectVulkan
#endif
void main() {
gl_Position = vec4(float(getLowBits(uint64_t(0x2au))));
}
)";
MG_Util::ShaderTranspiler::PreprocessShaderSource(ShaderStage::Vertex, source);
EXPECT_NE(source.find("#extension GL_ARB_gpu_shader_int64"), String::npos);
EXPECT_NE(source.find("GL_ARB_gpu_shader_int64"), String::npos);
auto shaderResult = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_VERTEX_SHADER,
.sourceStr = source,
.flags = MG_Util::ShaderTranspiler::ShaderCompileBits::CompileForOpenGL,
});
EXPECT_TRUE(shaderResult) << (shaderResult ? "" : shaderResult.error().log);
MG_Backend::pActiveBackendObject.reset();
}
// GL_MAX_VERTEX_ATTRIBS must follow the backend but never exceed the state layer's current-value
// storage: the DirectVulkan draw path indexes that array by shader input location, so advertising more
// than it can hold is an out-of-bounds read waiting to happen.
TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
using namespace MobileGL;
constexpr GLint capacity = MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
// A driver reporting more attributes than MobileGL can store gets clamped.
{
MG_Backend::DynamicBackendParameters params;
params.MaxVertexAttribs = capacity * 2;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
EXPECT_EQ(MG_Impl::GLImpl::VertexArrayImpl::GetMaxVertexAttribs(), static_cast<Uint>(capacity));
GLint reported = 0;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_ATTRIBS, &reported);
EXPECT_EQ(reported, capacity);
MG_Backend::pActiveBackendObject.reset();
}
// A driver below the capacity is followed exactly, and validation enforces that same bound.
{
MG_Backend::DynamicBackendParameters params;
params.MaxVertexAttribs = 16;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
EXPECT_EQ(MG_Impl::GLImpl::VertexArrayImpl::GetMaxVertexAttribs(), 16u);
GLint reported = 0;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_ATTRIBS, &reported);
EXPECT_EQ(reported, 16);
MG_State::pGLContext->ClearErrors();
EXPECT_FALSE(MG_Impl::GLImpl::VertexArrayImpl::ValidateVertexAttributeIndex(16));
EXPECT_TRUE(MG_State::pGLContext->HasGLError());
MG_State::pGLContext->ClearErrors();
EXPECT_TRUE(MG_Impl::GLImpl::VertexArrayImpl::ValidateVertexAttributeIndex(15));
EXPECT_FALSE(MG_State::pGLContext->HasGLError());
MG_Backend::pActiveBackendObject.reset();
}
// With no active backend the storage capacity is the bound, and nothing dereferences a null backend.
EXPECT_EQ(MG_Impl::GLImpl::VertexArrayImpl::GetMaxVertexAttribs(), static_cast<Uint>(capacity));
MG_State::pGLContext.reset();
}
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
using namespace MobileGL;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::DynamicBackendParameters params;
params.SubgroupSize = 32;
params.SubgroupSupportedStages = GL_VERTEX_SHADER_BIT | GL_FRAGMENT_SHADER_BIT | GL_COMPUTE_SHADER_BIT;
params.SubgroupSupportedFeatures = GL_SUBGROUP_FEATURE_BASIC_BIT_KHR |
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR |
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR |
GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
params.SubgroupQuadOperationsInAllStages = true;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
GLint intValue = 0;
MG_Impl::GLImpl::GetIntegerv(GL_SUBGROUP_SIZE_KHR, &intValue);
EXPECT_EQ(intValue, 32);
MG_Impl::GLImpl::GetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &intValue);
EXPECT_EQ(intValue, static_cast<GLint>(params.SubgroupSupportedStages));
MG_Impl::GLImpl::GetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &intValue);
EXPECT_EQ(intValue, static_cast<GLint>(params.SubgroupSupportedFeatures));
MG_Impl::GLImpl::GetIntegerv(GL_SUBGROUP_QUAD_ALL_STAGES_KHR, &intValue);
EXPECT_EQ(intValue, GL_TRUE);
GLint64 int64Value = 0;
MG_Impl::GLImpl::GetInteger64v(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &int64Value);
EXPECT_EQ(int64Value, static_cast<GLint64>(params.SubgroupSupportedFeatures));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset();
}
TEST(DirectVulkanSanity, CommandMemoryBarrierMakesIndirectDrawCommandsVisible) {
using namespace MobileGL;
using namespace MobileGL::MG_Backend::DirectVulkan;
const VkMemoryBarrier commandBarrier =
VulkanRenderer::BuildMemoryBarrierForGlBarriers(GL_COMMAND_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
EXPECT_NE(commandBarrier.dstAccessMask & VK_ACCESS_INDIRECT_COMMAND_READ_BIT, 0u);
const VkMemoryBarrier storageOnlyBarrier =
VulkanRenderer::BuildMemoryBarrierForGlBarriers(GL_SHADER_STORAGE_BARRIER_BIT);
EXPECT_EQ(storageOnlyBarrier.dstAccessMask & VK_ACCESS_INDIRECT_COMMAND_READ_BIT, 0u);
}
TEST(DirectVulkanSanity, DrawIndexedIndirectCommandMatchesGlAndVulkanLayout) {
using namespace MobileGL::MG_Backend::DirectVulkan;
EXPECT_EQ(sizeof(DrawIndexedCmdParam), 20u);
EXPECT_EQ(offsetof(DrawIndexedCmdParam, indexCount), 0u);
EXPECT_EQ(offsetof(DrawIndexedCmdParam, instanceCount), 4u);
EXPECT_EQ(offsetof(DrawIndexedCmdParam, firstIndex), 8u);
EXPECT_EQ(offsetof(DrawIndexedCmdParam, vertexOffset), 12u);
EXPECT_EQ(offsetof(DrawIndexedCmdParam, firstInstance), 16u);
}
TEST(DirectVulkanSanity, UndefinedDepthStencilLayoutUsesDontCareForUnclearedAspects) {
using namespace MobileGL;
using namespace MobileGL::MG_Backend::DirectVulkan;
auto noClear = ResolveDepthStencilAttachmentLoadInfo(VK_IMAGE_LAYOUT_UNDEFINED, false, false);
EXPECT_EQ(noClear.depthLoadOp, VK_ATTACHMENT_LOAD_OP_DONT_CARE);
EXPECT_EQ(noClear.stencilLoadOp, VK_ATTACHMENT_LOAD_OP_DONT_CARE);
EXPECT_EQ(noClear.initialLayout, VK_IMAGE_LAYOUT_UNDEFINED);
auto depthOnlyClear = ResolveDepthStencilAttachmentLoadInfo(VK_IMAGE_LAYOUT_UNDEFINED, true, false);
EXPECT_EQ(depthOnlyClear.depthLoadOp, VK_ATTACHMENT_LOAD_OP_CLEAR);
EXPECT_EQ(depthOnlyClear.stencilLoadOp, VK_ATTACHMENT_LOAD_OP_DONT_CARE);
EXPECT_EQ(depthOnlyClear.initialLayout, VK_IMAGE_LAYOUT_UNDEFINED);
auto knownLayout = ResolveDepthStencilAttachmentLoadInfo(
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, false, false);
EXPECT_EQ(knownLayout.depthLoadOp, VK_ATTACHMENT_LOAD_OP_LOAD);
EXPECT_EQ(knownLayout.stencilLoadOp, VK_ATTACHMENT_LOAD_OP_LOAD);
EXPECT_EQ(knownLayout.initialLayout, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
}
TEST(DirectVulkanSanity, SampledDepthStencilViewUsesSingleDepthAspect) {
using namespace MobileGL::MG_Backend::DirectVulkan;
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(VK_IMAGE_ASPECT_COLOR_BIT),
VK_IMAGE_ASPECT_COLOR_BIT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(VK_IMAGE_ASPECT_DEPTH_BIT),
VK_IMAGE_ASPECT_DEPTH_BIT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(VK_IMAGE_ASPECT_STENCIL_BIT),
VK_IMAGE_ASPECT_STENCIL_BIT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(
VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT),
VK_IMAGE_ASPECT_DEPTH_BIT);
}
TEST(RenderStateSanity, ProvokingVertexUpdatesStateAndValidatesEnum) {
using namespace MobileGL;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::pActiveBackendObject = MakeUnique<MG_Backend::DirectGLES::BackendObject_DirectGLES>();
GLint mode = 0;
MG_Impl::GLImpl::GetIntegerv(GL_PROVOKING_VERTEX, &mode);
EXPECT_EQ(mode, GL_LAST_VERTEX_CONVENTION);
const Uint initialVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
MG_Impl::GLImpl::ProvokingVertex(GL_FIRST_VERTEX_CONVENTION);
MG_Impl::GLImpl::GetIntegerv(GL_PROVOKING_VERTEX, &mode);
EXPECT_EQ(mode, GL_FIRST_VERTEX_CONVENTION);
EXPECT_GT(MG_State::pGLContext->GetRenderStateParametersVersion(), initialVersion);
const Uint updatedVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
MG_Impl::GLImpl::ProvokingVertex(GL_FIRST_VERTEX_CONVENTION);
EXPECT_EQ(MG_State::pGLContext->GetRenderStateParametersVersion(), updatedVersion);
MG_Impl::GLImpl::ProvokingVertex(GL_TRIANGLES);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM);
MG_Impl::GLImpl::GetIntegerv(GL_PROVOKING_VERTEX, &mode);
EXPECT_EQ(mode, GL_FIRST_VERTEX_CONVENTION);
MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset();
}
TEST(LogSanity, UsesEnvOverrideForFilePath) {
namespace fs = std::filesystem;
MobileGL::MG_Util::Debug::Close();
const fs::path logPath = fs::temp_directory_path() / "mobilegl-log-env-override-test.log";
const std::string message = "mobilegl-log-env-override-regression";
fs::remove(logPath);
SetEnvVar("MOBILEGL_LOG_FILE_PATH", logPath.string().c_str());
MobileGL::MG_Util::Debug::Log("INFO", ANDROID_LOG_INFO, "%s", message.c_str());
MobileGL::MG_Util::Debug::Close();
UnsetEnvVar("MOBILEGL_LOG_FILE_PATH");
{
std::ifstream logFile(logPath);
ASSERT_TRUE(logFile.good());
const std::string contents((std::istreambuf_iterator<char>(logFile)), std::istreambuf_iterator<char>());
EXPECT_NE(contents.find(message), std::string::npos);
}
fs::remove(logPath);
}