mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Feat] (ShaderTranspiler, DirectGLES): support noperspective conformantly instead of stripping it - let the qualifier reach glslang as the core SPIR-V NoPerspective decoration (native on DirectVulkan; SPIRV-Cross emits ESSL noperspective + GL_NV_shader_noperspective_interpolation on DirectGLES), and for GLES devices lacking that extension add StripNoPerspectivePass to drop the decoration and fall back to smooth; the old naked substring erase discarded the interpolation shader packs need and mangled identifiers containing the word
This commit is contained in:
@@ -192,6 +192,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
@@ -2790,6 +2790,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &uboPrecisionSpirv;
|
||||
}
|
||||
|
||||
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
|
||||
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
|
||||
// GL_NV_shader_noperspective_interpolation : require`; on a driver without that
|
||||
// extension the require fails, so strip the decoration first and let the varying
|
||||
// fall back to smooth interpolation. Devices that have the extension keep the
|
||||
// decoration and get true screen-linear interpolation.
|
||||
Vector<unsigned int> noperspectiveSpirv;
|
||||
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::StripNoPerspectiveForEssl(
|
||||
*effectiveSpirv, noperspectiveSpirv) &&
|
||||
!noperspectiveSpirv.empty()) {
|
||||
effectiveSpirv = &noperspectiveSpirv;
|
||||
}
|
||||
|
||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||
|
||||
|
||||
@@ -1089,6 +1089,174 @@ TEST_F(ProgramUtilTest, CompileFragmentShaderWithDiscard) {
|
||||
}
|
||||
}
|
||||
|
||||
// noperspective is core desktop GLSL (1.30+) and maps to the SPIR-V NoPerspective decoration. It must
|
||||
// reach glslang (not be stripped as text) so the SPIR-V carries the decoration; SPIRV-Cross then emits
|
||||
// ESSL `noperspective` + the GL_NV_shader_noperspective_interpolation extension. Shader packs
|
||||
// (Iris/Complementary) depend on it, and KHR-GL33.glsl_noperspective fails if the result matches
|
||||
// smooth. This is the DirectGLES path with the NV extension available (SPIRV-Cross's default).
|
||||
TEST_F(ProgramUtilTest, NoperspectiveInterpolationSurvivesToEssl) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String fs = R"(#version 330 core
|
||||
noperspective in vec4 vColor;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vColor; }
|
||||
)";
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) FAIL() << "compile errc: " << res.error().errc << "\nlog: " << res.error().log;
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {res.value()}};
|
||||
auto program_res = ShaderCompiler::LinkProgram(programAttrib);
|
||||
if (!program_res) FAIL() << "link errc: " << program_res.error().errc << "\nlog: " << program_res.error().log;
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program_res.value()};
|
||||
auto bin_res = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
if (!bin_res) FAIL() << "spirv errc: " << bin_res.error().errc << "\nlog: " << bin_res.error().log;
|
||||
ASSERT_EQ(bin_res.value().size(), 1u);
|
||||
|
||||
SpvcSession session(bin_res.value()[0], SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile errc: " << essl.error().errc << "\nlog: " << essl.error().log;
|
||||
|
||||
EXPECT_NE(essl.value().find("noperspective"), String::npos)
|
||||
<< "noperspective was lost before it reached SPIR-V:\n" << essl.value();
|
||||
EXPECT_NE(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos)
|
||||
<< "SPIRV-Cross must require the NV extension for ES noperspective:\n" << essl.value();
|
||||
}
|
||||
|
||||
// The old handling was a naked substring erase of "noperspective", so any identifier that merely
|
||||
// contained those characters (a uniform named noperspectiveBlend, say) got mangled. Removing the
|
||||
// strip fixes it - glslang, which is identifier-aware, is the only thing that should see the keyword.
|
||||
TEST_F(ProgramUtilTest, PreprocessDoesNotCorruptIdentifiersContainingNoperspective) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String source = R"(#version 330 core
|
||||
uniform float noperspectiveBlend;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(noperspectiveBlend); }
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_NE(source.find("noperspectiveBlend"), String::npos)
|
||||
<< "identifier was corrupted by substring stripping:\n" << source;
|
||||
}
|
||||
|
||||
// The DirectGLES fallback for devices without GL_NV_shader_noperspective_interpolation: stripping the
|
||||
// NoPerspective decoration makes SPIRV-Cross emit a plain smooth varying with no `#extension … :
|
||||
// require`, so the shader still compiles (rendering as smooth) instead of being rejected by the driver.
|
||||
TEST_F(ProgramUtilTest, StripNoPerspectiveFallbackProducesPlainEssl) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String fs = R"(#version 330 core
|
||||
noperspective in vec4 vColor;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vColor; }
|
||||
)";
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) FAIL() << "compile errc: " << res.error().errc << "\nlog: " << res.error().log;
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {res.value()}};
|
||||
auto program_res = ShaderCompiler::LinkProgram(programAttrib);
|
||||
if (!program_res) FAIL() << "link errc: " << program_res.error().errc << "\nlog: " << program_res.error().log;
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program_res.value()};
|
||||
auto bin_res = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
if (!bin_res) FAIL() << "spirv errc: " << bin_res.error().errc << "\nlog: " << bin_res.error().log;
|
||||
ASSERT_EQ(bin_res.value().size(), 1u);
|
||||
|
||||
// Precondition: with the decoration present the default decompile requires the NV extension.
|
||||
{
|
||||
SpvcSession session(bin_res.value()[0], SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile errc: " << essl.error().errc;
|
||||
ASSERT_NE(essl.value().find("noperspective"), String::npos) << essl.value();
|
||||
}
|
||||
|
||||
// The fallback strips the decoration -> plain smooth ESSL, no extension require.
|
||||
Vector<Uint32> stripped;
|
||||
ASSERT_TRUE(ShaderCompiler::StripNoPerspectiveForEssl(bin_res.value()[0], stripped));
|
||||
ASSERT_FALSE(stripped.empty());
|
||||
|
||||
SpvcSession session(stripped, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile errc: " << essl.error().errc << "\nlog: " << essl.error().log;
|
||||
EXPECT_EQ(essl.value().find("noperspective"), String::npos)
|
||||
<< "the decoration should be gone:\n" << essl.value();
|
||||
EXPECT_EQ(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos)
|
||||
<< "no extension require without the decoration:\n" << essl.value();
|
||||
}
|
||||
|
||||
// Directly exercises BOTH decoration forms StripNoPerspectivePass handles: a plain-variable
|
||||
// OpDecorate NoPerspective (in-operand 1) and an interface-block-member OpMemberDecorate NoPerspective
|
||||
// (in-operand 2). The ESSL round-trip tests above use only a scalar input, so they never reach the
|
||||
// member-decorate branch, which a block varying like `in Block { noperspective vec4 c; }` (common in
|
||||
// shader packs) produces. Unrelated decorations (Flat, Location) must survive untouched.
|
||||
TEST_F(ProgramUtilTest, StripNoPerspectivePassRemovesBothDecorateForms) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String spirvText = R"(
|
||||
OpCapability Shader
|
||||
OpMemoryModel Logical GLSL450
|
||||
OpEntryPoint Fragment %main "main" %plainVar %blockVar %flatVar
|
||||
OpExecutionMode %main OriginUpperLeft
|
||||
OpName %main "main"
|
||||
OpDecorate %plainVar Location 0
|
||||
OpDecorate %plainVar NoPerspective
|
||||
OpMemberDecorate %Block 0 NoPerspective
|
||||
OpDecorate %blockVar Location 1
|
||||
OpDecorate %flatVar Location 2
|
||||
OpDecorate %flatVar Flat
|
||||
%void = OpTypeVoid
|
||||
%mainFn = OpTypeFunction %void
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%int = OpTypeInt 32 1
|
||||
%inV4Ptr = OpTypePointer Input %v4float
|
||||
%plainVar = OpVariable %inV4Ptr Input
|
||||
%Block = OpTypeStruct %v4float
|
||||
%inBlockPtr = OpTypePointer Input %Block
|
||||
%blockVar = OpVariable %inBlockPtr Input
|
||||
%inIntPtr = OpTypePointer Input %int
|
||||
%flatVar = OpVariable %inIntPtr Input
|
||||
%main = OpFunction %void None %mainFn
|
||||
%mainBody = OpLabel
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
)";
|
||||
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
Vector<uint32_t> inputBinary;
|
||||
ASSERT_TRUE(tools.Assemble(spirvText, &inputBinary));
|
||||
|
||||
const auto countNoPerspective = [](const String& text) {
|
||||
SizeT count = 0, offset = 0;
|
||||
while ((offset = text.find("NoPerspective", offset)) != String::npos) {
|
||||
++count;
|
||||
offset += std::strlen("NoPerspective");
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
String inputText;
|
||||
ASSERT_TRUE(tools.Disassemble(inputBinary, &inputText));
|
||||
ASSERT_EQ(countNoPerspective(inputText), 2u)
|
||||
<< "fixture must carry both a plain and a member NoPerspective:\n" << inputText;
|
||||
|
||||
Vector<uint32_t> outputBinary;
|
||||
ASSERT_TRUE(ShaderCompiler::StripNoPerspectiveForEssl(inputBinary, outputBinary));
|
||||
ASSERT_FALSE(outputBinary.empty());
|
||||
|
||||
String outputText;
|
||||
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||
EXPECT_EQ(countNoPerspective(outputText), 0u)
|
||||
<< "both NoPerspective decorations (OpDecorate and OpMemberDecorate) must be stripped:\n" << outputText;
|
||||
EXPECT_NE(outputText.find("Flat"), String::npos)
|
||||
<< "the unrelated Flat decoration must survive:\n" << outputText;
|
||||
EXPECT_NE(outputText.find("Location"), String::npos)
|
||||
<< "Location decorations must survive:\n" << outputText;
|
||||
}
|
||||
|
||||
const char* vs_location = R"(#version 460
|
||||
|
||||
in vec4 Position;
|
||||
|
||||
@@ -811,6 +811,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
if (std::strcmp(extension, "GL_EXT_blend_func_extended") == 0) {
|
||||
caps.SupportsDualSourceBlend = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
|
||||
caps.SupportsNoperspectiveInterpolation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1050,6 +1050,11 @@ namespace MobileGL {
|
||||
// factors and layout(index = 1) fragment outputs. GLES core has no dual-source blending,
|
||||
// so without this a draw using a SRC1 factor cannot proceed.
|
||||
Bool SupportsDualSourceBlend = false;
|
||||
// GL_NV_shader_noperspective_interpolation is present: the driver accepts the
|
||||
// `noperspective` interpolation qualifier in ESSL. GLES core has none, so without this
|
||||
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
|
||||
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
|
||||
Bool SupportsNoperspectiveInterpolation = false;
|
||||
// GL_RENDERER contains "ANGLE".
|
||||
Bool IsAngleRenderer = false;
|
||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
@@ -334,6 +335,18 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(StripNoPerspectivePass::CreateStripNoPerspectivePass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -33,6 +33,11 @@ namespace MobileGL {
|
||||
// Only for the DirectGLES transpile path.
|
||||
static bool StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Removes NoPerspective decorations so SPIRV-Cross emits plain (smooth) ESSL varyings.
|
||||
// DirectGLES fallback only, for devices lacking GL_NV_shader_noperspective_interpolation
|
||||
// (SPIRV-Cross would otherwise require that extension and the driver would reject it).
|
||||
static bool StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||
|
||||
@@ -1251,15 +1251,13 @@ namespace MobileGL {
|
||||
|
||||
NormalizeLineDirectives(source);
|
||||
|
||||
// remove "noperspective"
|
||||
const char* str_np = "noperspective";
|
||||
const SizeT len_np = strlen(str_np);
|
||||
SizeT noperspectivePos = source.find(str_np);
|
||||
while (noperspectivePos != String::npos) {
|
||||
// + length of "\n"
|
||||
source = source.replace(noperspectivePos, len_np, "");
|
||||
noperspectivePos = source.find(str_np);
|
||||
}
|
||||
// noperspective is intentionally NOT touched here. It is core in desktop GLSL (1.30+)
|
||||
// and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders
|
||||
// natively and SPIRV-Cross turns into ESSL `noperspective` + the
|
||||
// GL_NV_shader_noperspective_interpolation extension. The old naked substring erase
|
||||
// both discarded that interpolation (shader packs need it) and corrupted any
|
||||
// identifier that merely contained the word. The GLES fallback for devices without
|
||||
// the extension lives in the backend, where device capabilities are known.
|
||||
|
||||
FilterUnsupportedGpuShaderInt64(source);
|
||||
CoerceUniformBlockPackingToStd140(source);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.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 "StripNoPerspectivePass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
|
||||
// OpDecorate <target-id> <decoration> [literals...]
|
||||
// OpMemberDecorate <struct-id> <member> <decoration> [literals...]
|
||||
constexpr uint32_t kDecorateDecorationOperand = 1;
|
||||
constexpr uint32_t kMemberDecorateDecorationOperand = 2;
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status StripNoPerspectivePass::Process() {
|
||||
auto* irContext = context();
|
||||
|
||||
// Collect first: KillInst mutates the annotation list being walked.
|
||||
std::vector<Instruction*> toKill;
|
||||
for (Instruction& annotation : irContext->annotations()) {
|
||||
uint32_t decorationOperand = 0;
|
||||
if (annotation.opcode() == spv::Op::OpDecorate) {
|
||||
decorationOperand = kDecorateDecorationOperand;
|
||||
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
|
||||
decorationOperand = kMemberDecorateDecorationOperand;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (annotation.NumInOperands() <= decorationOperand) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(decorationOperand)) ==
|
||||
spv::Decoration::NoPerspective) {
|
||||
toKill.push_back(&annotation);
|
||||
}
|
||||
}
|
||||
|
||||
if (toKill.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
for (Instruction* inst : toKill) {
|
||||
irContext->KillInst(inst);
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken StripNoPerspectivePass::CreateStripNoPerspectivePass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<StripNoPerspectivePass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,35 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.h
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Removes the NoPerspective decoration from every interface variable and block member.
|
||||
// DirectGLES fallback only, for devices that lack GL_NV_shader_noperspective_interpolation:
|
||||
// SPIRV-Cross renders a NoPerspective-decorated varying as ESSL `noperspective` plus
|
||||
// `#extension GL_NV_shader_noperspective_interpolation : require`, which such a driver
|
||||
// rejects. Dropping the decoration falls the varying back to smooth (perspective-correct)
|
||||
// interpolation - the same visible result the old text-level strip produced, but without
|
||||
// corrupting identifiers and without touching DirectVulkan, where NoPerspective is native.
|
||||
// (The exact screen-linear emulation via gl_Position.w / gl_FragCoord.w is a later step.)
|
||||
class StripNoPerspectivePass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "strip-noperspective"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateStripNoPerspectivePass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user