mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-09 19:13:23 +09:00
184 lines
8.5 KiB
C++
184 lines
8.5 KiB
C++
// MobileGL - MobileGL/MG_Test/Pipeline/DriverModuleScreenTest.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
|
|
//
|
|
// ProgramFactory::ScreenModuleForDriver is the last look Magma takes at a module's bytes before
|
|
// vkCreateShaderModule. A driver is not a validating entry point: a module that carried a sampler
|
|
// inside the default uniform block (a frontend translation defect) was accepted by Adreno, which
|
|
// then page-faulted on a descriptor read out of uniform-buffer bytes and lost the device. The
|
|
// screen used to report such a module and hand it over anyway; it must DECLINE it, so the GPU
|
|
// never sees it, unless MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV asks otherwise.
|
|
//
|
|
// The invalid module here is that exact shape, assembled by hand so it stays invalid whatever
|
|
// the frontend does: a Block-decorated uniform struct with a sampled-image member.
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
|
|
#include "Includes.h"
|
|
#include "Init.h"
|
|
|
|
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
|
|
|
#include "spirv-tools/libspirv.hpp"
|
|
|
|
using namespace MobileGL;
|
|
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
|
using Verdict = ProgramFactory::DriverModuleVerdict;
|
|
|
|
namespace {
|
|
|
|
// Shared by both modules: a fragment shader whose default uniform block holds a vec4 and,
|
|
// in the invalid one, a sampler2D next to it.
|
|
String UniformBlockFragment(Bool withSampler) {
|
|
String text =
|
|
"OpCapability Shader\n"
|
|
"OpMemoryModel Logical GLSL450\n"
|
|
"OpEntryPoint Fragment %main \"main\" %out\n"
|
|
"OpExecutionMode %main OriginUpperLeft\n"
|
|
"OpDecorate %out Location 0\n"
|
|
"OpDecorate %Block Block\n"
|
|
"OpMemberDecorate %Block 0 Offset 0\n"
|
|
"OpDecorate %ubo DescriptorSet 0\n"
|
|
"OpDecorate %ubo Binding 0\n"
|
|
"%void = OpTypeVoid\n"
|
|
"%fn = OpTypeFunction %void\n"
|
|
"%float = OpTypeFloat 32\n"
|
|
"%v4 = OpTypeVector %float 4\n"
|
|
"%int = OpTypeInt 32 1\n"
|
|
"%zero = OpConstant %int 0\n"
|
|
"%img = OpTypeImage %float 2D 0 0 0 1 Unknown\n"
|
|
"%simg = OpTypeSampledImage %img\n";
|
|
text += withSampler ? "%Block = OpTypeStruct %v4 %simg\n" : "%Block = OpTypeStruct %v4\n";
|
|
text +=
|
|
"%pBlock = OpTypePointer Uniform %Block\n"
|
|
"%ubo = OpVariable %pBlock Uniform\n"
|
|
"%pv4u = OpTypePointer Uniform %v4\n"
|
|
"%pv4o = OpTypePointer Output %v4\n"
|
|
"%out = OpVariable %pv4o Output\n"
|
|
"%main = OpFunction %void None %fn\n"
|
|
"%entry = OpLabel\n"
|
|
"%ptr = OpAccessChain %pv4u %ubo %zero\n"
|
|
"%color = OpLoad %v4 %ptr\n"
|
|
"OpStore %out %color\n"
|
|
"OpReturn\n"
|
|
"OpFunctionEnd\n";
|
|
return text;
|
|
}
|
|
|
|
Vector<Uint> Assemble(const String& text) {
|
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
std::vector<uint32_t> words;
|
|
EXPECT_TRUE(tools.Assemble(text, &words)) << text;
|
|
return Vector<Uint>(words.begin(), words.end());
|
|
}
|
|
|
|
// A realistic module out of the real frontend, for the cost measurement.
|
|
Vector<Uint> CompileFragment(const String& source) {
|
|
using namespace MG_Util::ShaderTranspiler;
|
|
ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
|
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
|
if (!shaderResult) return {};
|
|
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
|
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
|
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
|
if (!programResult) return {};
|
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *programResult.value()};
|
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
if (!binaryResult || binaryResult->empty()) return {};
|
|
const auto& words = binaryResult->front();
|
|
return Vector<Uint>(words.begin(), words.end());
|
|
}
|
|
|
|
class DriverModuleScreenTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override { MobileGL::Initialize(); }
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// The headline: the sampler-in-uniform-block module never reaches the driver. Red before the
|
|
// screen acted on its verdict - it answered AdmittedInvalid, i.e. "logged, handed over anyway".
|
|
TEST_F(DriverModuleScreenTest, ASamplerInsideTheDefaultUniformBlockIsDeclined) {
|
|
const Vector<Uint> invalid = Assemble(UniformBlockFragment(/*withSampler=*/true));
|
|
ASSERT_FALSE(invalid.empty());
|
|
String reason;
|
|
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(invalid, /*allowInvalid=*/false, &reason),
|
|
Verdict::DeclinedInvalid);
|
|
EXPECT_FALSE(reason.empty()) << "a decline must carry the validator's reason for its log line";
|
|
}
|
|
|
|
// The positive control: the same block without the sampler validates and is handed over. A screen
|
|
// that declined everything would pass the case above and fail this one.
|
|
TEST_F(DriverModuleScreenTest, TheSameBlockWithoutTheSamplerPasses) {
|
|
const Vector<Uint> valid = Assemble(UniformBlockFragment(/*withSampler=*/false));
|
|
ASSERT_FALSE(valid.empty());
|
|
String reason = "stale";
|
|
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(valid, /*allowInvalid=*/false, &reason), Verdict::Valid);
|
|
EXPECT_TRUE(reason.empty());
|
|
}
|
|
|
|
// The escape hatch: with MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV the invalid module is handed over,
|
|
// and the verdict says it was invalid rather than pretending it validated.
|
|
TEST_F(DriverModuleScreenTest, TheEscapeHatchAdmitsAnInvalidModuleAndSaysSo) {
|
|
const Vector<Uint> invalid = Assemble(UniformBlockFragment(/*withSampler=*/true));
|
|
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(invalid, /*allowInvalid=*/true), Verdict::AdmittedInvalid);
|
|
const Vector<Uint> valid = Assemble(UniformBlockFragment(/*withSampler=*/false));
|
|
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(valid, /*allowInvalid=*/true), Verdict::Valid);
|
|
}
|
|
|
|
// The verdict is remembered by content: re-screening the same bytes does not run spirv-val again,
|
|
// and a different module does. Also reports what one validation costs next to a cached answer.
|
|
TEST_F(DriverModuleScreenTest, TheVerdictIsCachedPerModuleContent) {
|
|
const Vector<Uint> module = CompileFragment(R"(#version 330 core
|
|
uniform sampler2D tex0;
|
|
uniform sampler2D tex1;
|
|
uniform vec4 tint;
|
|
uniform mat4 colorMatrix;
|
|
in vec2 uv;
|
|
in vec4 vertexColor;
|
|
out vec4 fragColor;
|
|
vec3 tonemap(vec3 c) { return c / (c + vec3(1.0)); }
|
|
void main() {
|
|
vec4 base = texture(tex0, uv) * vertexColor;
|
|
vec4 detail = texture(tex1, uv * 4.0);
|
|
vec4 c = colorMatrix * mix(base, base * detail, 0.5) * tint;
|
|
for (int i = 0; i < 4; ++i) c.rgb += 0.01 * sin(c.gbr * float(i + 1));
|
|
fragColor = vec4(tonemap(c.rgb), c.a);
|
|
}
|
|
)");
|
|
ASSERT_FALSE(module.empty());
|
|
// A copy with one changed word in the bound field of the header: different bytes, same
|
|
// validity, so it must miss the cache.
|
|
Vector<Uint> sibling = module;
|
|
sibling[3] += 1;
|
|
|
|
const Uint64 runsBefore = ProgramFactory::DriverModuleValidatorRuns();
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
const Verdict first = ProgramFactory::ScreenModuleForDriver(module, false);
|
|
const auto t1 = std::chrono::steady_clock::now();
|
|
const Verdict second = ProgramFactory::ScreenModuleForDriver(module, false);
|
|
const auto t2 = std::chrono::steady_clock::now();
|
|
EXPECT_EQ(first, Verdict::Valid);
|
|
EXPECT_EQ(second, first);
|
|
EXPECT_EQ(ProgramFactory::DriverModuleValidatorRuns(), runsBefore + 1) << "the second screen re-validated";
|
|
|
|
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(sibling, false), Verdict::Valid);
|
|
EXPECT_EQ(ProgramFactory::DriverModuleValidatorRuns(), runsBefore + 2) << "different bytes shared a verdict";
|
|
|
|
const auto us = [](auto a, auto b) {
|
|
return static_cast<long long>(std::chrono::duration_cast<std::chrono::microseconds>(b - a).count());
|
|
};
|
|
std::printf("[ screen ] %zu-word module: validate %lld us, cached %lld us\n", module.size(), us(t0, t1),
|
|
us(t1, t2));
|
|
}
|