mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
184 lines
8.2 KiB
C++
184 lines
8.2 KiB
C++
// MobileGL - MobileGL/MG_Test/Pipeline/ViewportIndexReflectionTest.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::ReflectedWritesViewportIndexBuiltin is the switch that decides whether a
|
|
// DirectVulkan pipeline declares one viewport or all sixteen. Getting it wrong is silent in both
|
|
// directions and neither direction is caught by a state test:
|
|
//
|
|
// - a false NEGATIVE collapses every gl_ViewportIndex onto viewport 0, which is precisely the
|
|
// bug the multi-viewport work exists to fix and which a set/get round trip cannot see;
|
|
// - a false POSITIVE widens viewportCount for an ordinary Minecraft shader, costing a longer
|
|
// vkCmdSetViewport per state change and, on a tiler, possibly a hardware fast path.
|
|
//
|
|
// So this compiles REAL GLSL through the same glslang path the renderer uses and reflects the
|
|
// SPIR-V that comes out, rather than asserting against hand-assembled words: what has to hold is
|
|
// that the detector agrees with what glslang actually emits for a shader that writes the builtin,
|
|
// including the stage-by-stage question of WHERE it may be written (GL 4.1 allows the geometry
|
|
// stage; ARB_shader_viewport_layer_array adds vertex and tessellation evaluation).
|
|
//
|
|
// The end-to-end claim - that a detected writer really does route pixels to its own viewport -
|
|
// lives in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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_reflect.h>
|
|
|
|
using namespace MobileGL;
|
|
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
|
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
|
|
|
namespace {
|
|
|
|
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
|
using namespace MG_Util::ShaderTranspiler;
|
|
ShaderAttrib shaderAttrib{.shaderType = stage, .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 = {stage}, .program = *programResult.value()};
|
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
|
if (!binaryResult || binaryResult->empty()) return {};
|
|
return binaryResult->front();
|
|
}
|
|
|
|
// Owns the reflection module so a failing EXPECT cannot leak it.
|
|
class ReflectModule {
|
|
public:
|
|
explicit ReflectModule(const Vector<Uint32>& spirv) {
|
|
if (spirv.empty()) return;
|
|
m_created = spvReflectCreateShaderModule(spirv.size() * sizeof(Uint32), spirv.data(), &m_module) ==
|
|
SPV_REFLECT_RESULT_SUCCESS;
|
|
}
|
|
~ReflectModule() {
|
|
if (m_created) spvReflectDestroyShaderModule(&m_module);
|
|
}
|
|
ReflectModule(const ReflectModule&) = delete;
|
|
ReflectModule& operator=(const ReflectModule&) = delete;
|
|
|
|
Bool Created() const { return m_created; }
|
|
const SpvReflectShaderModule& Get() const { return m_module; }
|
|
|
|
private:
|
|
SpvReflectShaderModule m_module{};
|
|
Bool m_created = false;
|
|
};
|
|
|
|
class ViewportIndexReflectionTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override { MobileGL::Initialize(); }
|
|
};
|
|
|
|
const char* const kGeometryWritesViewportIndex = R"(#version 410 core
|
|
layout(points, invocations = 16) in;
|
|
layout(triangle_strip, max_vertices = 4) out;
|
|
void main() {
|
|
gl_ViewportIndex = gl_InvocationID;
|
|
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
EndPrimitive();
|
|
}
|
|
)";
|
|
|
|
// Same stage, same shape, writing gl_Layer INSTEAD. Layered rendering and viewport routing
|
|
// are different features and the detector must not confuse them: a Minecraft-style cubemap
|
|
// pass writes gl_Layer and must keep the one-viewport pipeline.
|
|
const char* const kGeometryWritesLayerOnly = R"(#version 410 core
|
|
layout(points, invocations = 6) in;
|
|
layout(triangle_strip, max_vertices = 4) out;
|
|
void main() {
|
|
gl_Layer = gl_InvocationID;
|
|
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
EndPrimitive();
|
|
}
|
|
)";
|
|
|
|
const char* const kPlainGeometry = R"(#version 410 core
|
|
layout(points, invocations = 1) in;
|
|
layout(triangle_strip, max_vertices = 4) out;
|
|
void main() {
|
|
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
EndPrimitive();
|
|
}
|
|
)";
|
|
|
|
const char* const kPlainVertex = R"(#version 410 core
|
|
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
|
)";
|
|
|
|
const char* const kPlainFragment = R"(#version 410 core
|
|
layout(location = 0) out vec4 fragColor;
|
|
void main() { fragColor = vec4(1.0); }
|
|
)";
|
|
|
|
TEST_F(ViewportIndexReflectionTest, TrueForAGeometryShaderThatAssignsViewportIndex) {
|
|
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex));
|
|
ASSERT_TRUE(module.Created());
|
|
EXPECT_TRUE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
|
<< "a shader that assigns gl_ViewportIndex must get a multi-viewport pipeline; missing it is what "
|
|
"collapses every index onto viewport 0";
|
|
}
|
|
|
|
TEST_F(ViewportIndexReflectionTest, FalseForAGeometryShaderThatOnlyAssignsLayer) {
|
|
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly));
|
|
ASSERT_TRUE(module.Created());
|
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
|
<< "gl_Layer is layered rendering, not viewport routing; widening viewportCount for it costs the "
|
|
"single-viewport fast path for nothing";
|
|
}
|
|
|
|
TEST_F(ViewportIndexReflectionTest, FalseForAPlainGeometryShader) {
|
|
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry));
|
|
ASSERT_TRUE(module.Created());
|
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()));
|
|
}
|
|
|
|
TEST_F(ViewportIndexReflectionTest, FalseForTheOrdinaryVertexAndFragmentStages) {
|
|
// The shape every real application ships: neither stage may widen the pipeline.
|
|
const ReflectModule vertexModule(CompileToSpirv(GL_VERTEX_SHADER, kPlainVertex));
|
|
ASSERT_TRUE(vertexModule.Created());
|
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(vertexModule.Get()));
|
|
|
|
const ReflectModule fragmentModule(CompileToSpirv(GL_FRAGMENT_SHADER, kPlainFragment));
|
|
ASSERT_TRUE(fragmentModule.Created());
|
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(fragmentModule.Get()));
|
|
}
|
|
|
|
TEST_F(ViewportIndexReflectionTest, FalseForAnEmptyModuleWithoutDereferencing) {
|
|
// A default-constructed module has no entry points. The scan runs on every link, so it
|
|
// must survive a reflection that never got built rather than walk a null array.
|
|
SpvReflectShaderModule emptyModule{};
|
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(emptyModule));
|
|
}
|
|
|
|
} // namespace
|