Metal backend pt3: shader translator

Implementation of GLSL to MSL translator

Bug: angleproject:2634
Change-Id: I66e2374b461548fac46163ea79790a488515e6b0
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/1887251
Commit-Queue: Jamie Madill <jmadill@chromium.org>
Reviewed-by: Jamie Madill <jmadill@chromium.org>
This commit is contained in:
Le Quyen 2019-10-29 22:57:55 +08:00 коммит произвёл Commit Bot
Родитель 653ee5f115
Коммит a5a04ac0f9
14 изменённых файлов: 392 добавлений и 60 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -40,6 +40,7 @@
/third_party/qemu-linux-x64
/third_party/qemu-mac-x64
/third_party/rapidjson/src
/third_party/spirv-cross/src
/third_party/spirv-headers/src
/third_party/spirv-tools/src
/third_party/SwiftShader

8
DEPS
Просмотреть файл

@ -41,6 +41,9 @@ vars = {
# Note: this dep cannot be auto-rolled b/c of nesting.
'patched_yasm_revision': '720b70524a4424b15fc57e82263568c8ba0496ad',
# Current revision of spirv-cross, the Khronos SPIRV cross compiler.
'spirv_cross_revision': 'd253f41e17e27285756d031d8ba43bf370264e1f',
# Current revision fo the SPIRV-Headers Vulkan support library.
'spirv_headers_revision': 'af64a9e826bf5bb5fcd2434dd71be1e41e922563',
@ -160,6 +163,11 @@ deps = {
'url': '{chromium_git}/external/github.com/Tencent/rapidjson@7484e06c589873e1ed80382d262087e4fa80fb63',
},
'third_party/spirv-cross/src': {
'url': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Cross@{spirv_cross_revision}',
'condition': 'not build_with_chromium',
},
'third_party/spirv-headers/src': {
'url': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Headers@{spirv_headers_revision}',
'condition': 'not build_with_chromium',

Просмотреть файл

@ -14,4 +14,5 @@ angle_googletest_dir = "//third_party/googletest/src"
angle_libjpeg_turbo_dir = "//third_party/libjpeg_turbo"
angle_jsoncpp_dir = "//third_party/jsoncpp"
angle_libpng_dir = "//third_party/libpng"
angle_spirv_cross_dir = "//third_party/spirv-cross/src"
angle_spirv_tools_dir = "//third_party/spirv-tools/src"

Просмотреть файл

@ -25,6 +25,12 @@ class TOutputVulkanGLSLForMetal : public TOutputVulkanGLSL
int shaderVersion,
ShShaderOutput output,
ShCompileOptions compileOptions);
static void RemoveInvariantForTest(bool remove);
protected:
bool visitInvariantDeclaration(Visit visit, TIntermInvariantDeclaration *node) override;
void writeVariableType(const TType &type, const TSymbol *symbol) override;
};
} // namespace sh

Просмотреть файл

@ -10,11 +10,50 @@
#include "compiler/translator/OutputVulkanGLSLForMetal.h"
#include "common/debug.h"
#include "common/apple_platform_utils.h"
#include "compiler/translator/BaseTypes.h"
#include "compiler/translator/Symbol.h"
#include "compiler/translator/util.h"
namespace sh
{
namespace
{
bool gOverrideRemoveInvariant = false;
bool ShoudRemoveInvariant(const TType &type)
{
if (gOverrideRemoveInvariant)
{
return true;
}
if (type.getQualifier() != EvqPosition)
{
// Metal only supports invariant for gl_Position
return true;
}
if (ANGLE_APPLE_AVAILABLE_XCI(10.14, 13.0, 12))
{
return false;
}
else
{
// Metal 2.1 is not available, so we need to remove "invariant" keyword
return true;
}
}
}
// static
void TOutputVulkanGLSLForMetal::RemoveInvariantForTest(bool remove)
{
gOverrideRemoveInvariant = remove;
}
TOutputVulkanGLSLForMetal::TOutputVulkanGLSLForMetal(TInfoSinkBase &objSink,
ShArrayIndexClampingStrategy clampingStrategy,
ShHashFunction64 hashFunction,
@ -33,8 +72,32 @@ TOutputVulkanGLSLForMetal::TOutputVulkanGLSLForMetal(TInfoSinkBase &objSink,
shaderVersion,
output,
compileOptions)
{}
void TOutputVulkanGLSLForMetal::writeVariableType(const TType &type, const TSymbol *symbol)
{
UNIMPLEMENTED();
TType overrideType(type);
// Remove invariant keyword if required.
if (type.isInvariant() && ShoudRemoveInvariant(type))
{
overrideType.setInvariant(false);
}
TOutputVulkanGLSL::writeVariableType(overrideType, symbol);
}
bool TOutputVulkanGLSLForMetal::visitInvariantDeclaration(Visit visit,
TIntermInvariantDeclaration *node)
{
TInfoSinkBase &out = objSink();
ASSERT(visit == PreVisit);
const TIntermSymbol *symbol = node->getSymbol();
if (!ShoudRemoveInvariant(symbol->getType()))
{
out << "invariant ";
}
out << hashName(&symbol->variable());
return false;
}
} // namespace sh

Просмотреть файл

@ -4,40 +4,96 @@
// found in the LICENSE file.
//
// TranslatorMetal:
// Translator for Metal backend.
// A GLSL-based translator that outputs shaders that fit GL_KHR_vulkan_glsl.
// It takes into account some considerations for Metal backend also.
// The shaders are then fed into glslang to spit out SPIR-V (libANGLE-side).
// See: https://www.khronos.org/registry/vulkan/specs/misc/GL_KHR_vulkan_glsl.txt
//
// The SPIR-V will then be translated to Metal Shading Language later in Metal backend.
//
#include "compiler/translator/TranslatorMetal.h"
#include "common/debug.h"
#include "angle_gl.h"
#include "common/utilities.h"
#include "compiler/translator/OutputVulkanGLSLForMetal.h"
#include "compiler/translator/StaticType.h"
#include "compiler/translator/tree_util/BuiltIn.h"
#include "compiler/translator/tree_util/RunAtTheEndOfShader.h"
#include "compiler/translator/util.h"
namespace sh
{
TranslatorMetal::TranslatorMetal(sh::GLenum type, ShShaderSpec spec)
: TCompiler(type, spec, SH_GLSL_450_CORE_OUTPUT)
namespace
{
// Unlike Vulkan having auto viewport flipping extension, in Metal we have to flip gl_Position.y
// manually.
// This operation performs flipping the gl_Position.y using this expression:
// gl_Position.y = gl_Position.y * negViewportScaleY
ANGLE_NO_DISCARD bool AppendVertexShaderPositionYCorrectionToMain(TCompiler *compiler,
TIntermBlock *root,
TSymbolTable *symbolTable,
TIntermBinary *negViewportYScale)
{
// Create a symbol reference to "gl_Position"
const TVariable *position = BuiltInVariable::gl_Position();
TIntermSymbol *positionRef = new TIntermSymbol(position);
// Create a swizzle to "gl_Position.y"
TVector<int> swizzleOffsetY;
swizzleOffsetY.push_back(1);
TIntermSwizzle *positionY = new TIntermSwizzle(positionRef, swizzleOffsetY);
// Create the expression "gl_Position.y * negViewportScaleY"
TIntermBinary *inverseY = new TIntermBinary(EOpMul, positionY->deepCopy(), negViewportYScale);
// Create the assignment "gl_Position.y = gl_Position.y * negViewportScaleY
TIntermTyped *positionYLHS = positionY->deepCopy();
TIntermBinary *assignment = new TIntermBinary(TOperator::EOpAssign, positionYLHS, inverseY);
// Append the assignment as a statement at the end of the shader.
return RunAtTheEndOfShader(compiler, root, assignment, symbolTable);
}
} // anonymous namespace
TranslatorMetal::TranslatorMetal(sh::GLenum type, ShShaderSpec spec) : TranslatorVulkan(type, spec)
{}
bool TranslatorMetal::translate(TIntermBlock *root,
ShCompileOptions compileOptions,
PerformanceDiagnostics * /*perfDiagnostics*/)
PerformanceDiagnostics *perfDiagnostics)
{
TInfoSinkBase &sink = getInfoSink().obj;
TOutputVulkanGLSLForMetal outputGLSL(sink, getArrayIndexClampingStrategy(), getHashFunction(),
getNameMap(), &getSymbolTable(), getShaderType(),
getShaderVersion(), getOutputType(), compileOptions);
TOutputVulkanGLSL outputGLSL(sink, getArrayIndexClampingStrategy(), getHashFunction(),
getNameMap(), &getSymbolTable(), getShaderType(),
getShaderVersion(), getOutputType(), compileOptions);
UNIMPLEMENTED();
const TVariable *driverUniforms = nullptr;
if (!TranslatorVulkan::translateImpl(root, compileOptions, perfDiagnostics, &driverUniforms,
&outputGLSL))
{
return false;
}
if (getShaderType() == GL_VERTEX_SHADER)
{
auto negViewportYScale = getDriverUniformNegViewportYScaleRef(driverUniforms);
// Append gl_Position.y correction to main
if (!AppendVertexShaderPositionYCorrectionToMain(this, root, &getSymbolTable(),
negViewportYScale))
{
return false;
}
}
// Write translated shader.
root->traverse(&outputGLSL);
return false;
}
bool TranslatorMetal::shouldFlattenPragmaStdglInvariantAll()
{
UNIMPLEMENTED();
return false;
return true;
}
} // namespace sh

Просмотреть файл

@ -4,18 +4,23 @@
// found in the LICENSE file.
//
// TranslatorMetal:
// Translator for Metal backend.
// A GLSL-based translator that outputs shaders that fit GL_KHR_vulkan_glsl.
// It takes into account some considerations for Metal backend also.
// The shaders are then fed into glslang to spit out SPIR-V (libANGLE-side).
// See: https://www.khronos.org/registry/vulkan/specs/misc/GL_KHR_vulkan_glsl.txt
//
// The SPIR-V will then be translated to Metal Shading Language later in Metal backend.
//
#ifndef LIBANGLE_RENDERER_METAL_TRANSLATORMETAL_H_
#define LIBANGLE_RENDERER_METAL_TRANSLATORMETAL_H_
#include "compiler/translator/Compiler.h"
#include "compiler/translator/TranslatorVulkan.h"
namespace sh
{
class TranslatorMetal : public TCompiler
class TranslatorMetal : public TranslatorVulkan
{
public:
TranslatorMetal(sh::GLenum type, ShShaderSpec spec);
@ -24,8 +29,6 @@ class TranslatorMetal : public TCompiler
ANGLE_NO_DISCARD bool translate(TIntermBlock *root,
ShCompileOptions compileOptions,
PerformanceDiagnostics *perfDiagnostics) override;
bool shouldFlattenPragmaStdglInvariantAll() override;
};
} // namespace sh

Просмотреть файл

@ -656,14 +656,13 @@ TranslatorVulkan::TranslatorVulkan(sh::GLenum type, ShShaderSpec spec)
: TCompiler(type, spec, SH_GLSL_450_CORE_OUTPUT)
{}
bool TranslatorVulkan::translate(TIntermBlock *root,
ShCompileOptions compileOptions,
PerformanceDiagnostics * /*perfDiagnostics*/)
bool TranslatorVulkan::translateImpl(TIntermBlock *root,
ShCompileOptions compileOptions,
PerformanceDiagnostics * /*perfDiagnostics*/,
const TVariable **driverUniformsOut,
TOutputVulkanGLSL *outputGLSL)
{
TInfoSinkBase &sink = getInfoSink().obj;
TOutputVulkanGLSL outputGLSL(sink, getArrayIndexClampingStrategy(), getHashFunction(),
getNameMap(), &getSymbolTable(), getShaderType(),
getShaderVersion(), getOutputType(), compileOptions);
if (getShaderType() == GL_VERTEX_SHADER)
{
@ -727,7 +726,7 @@ bool TranslatorVulkan::translate(TIntermBlock *root,
defaultUniformCount -= removedUniformsCount;
// We must declare the struct types before using them.
DeclareStructTypesTraverser structTypesTraverser(&outputGLSL);
DeclareStructTypesTraverser structTypesTraverser(outputGLSL);
root->traverse(&structTypesTraverser);
if (!structTypesTraverser.updateTree(this, root))
{
@ -939,6 +938,29 @@ bool TranslatorVulkan::translate(TIntermBlock *root,
return false;
}
if (driverUniformsOut)
{
*driverUniformsOut = driverUniforms;
}
return true;
}
bool TranslatorVulkan::translate(TIntermBlock *root,
ShCompileOptions compileOptions,
PerformanceDiagnostics *perfDiagnostics)
{
TInfoSinkBase &sink = getInfoSink().obj;
TOutputVulkanGLSL outputGLSL(sink, getArrayIndexClampingStrategy(), getHashFunction(),
getNameMap(), &getSymbolTable(), getShaderType(),
getShaderVersion(), getOutputType(), compileOptions);
if (!translateImpl(root, compileOptions, perfDiagnostics, nullptr, &outputGLSL))
{
return false;
}
// Write translated shader.
root->traverse(&outputGLSL);
@ -951,4 +973,10 @@ bool TranslatorVulkan::shouldFlattenPragmaStdglInvariantAll()
return false;
}
TIntermBinary *TranslatorVulkan::getDriverUniformNegViewportYScaleRef(
const TVariable *driverUniforms) const
{
return CreateDriverUniformRef(driverUniforms, kNegViewportYScale);
}
} // namespace sh

Просмотреть файл

@ -17,6 +17,8 @@
namespace sh
{
class TOutputVulkanGLSL;
class TranslatorVulkan : public TCompiler
{
public:
@ -27,6 +29,15 @@ class TranslatorVulkan : public TCompiler
ShCompileOptions compileOptions,
PerformanceDiagnostics *perfDiagnostics) override;
bool shouldFlattenPragmaStdglInvariantAll() override;
TIntermBinary *getDriverUniformNegViewportYScaleRef(const TVariable *driverUniforms) const;
// Subclass can call this method to transform the AST before writing the final output.
// See TranslatorMetal.cpp.
ANGLE_NO_DISCARD bool translateImpl(TIntermBlock *root,
ShCompileOptions compileOptions,
PerformanceDiagnostics *perfDiagnostics,
const TVariable **driverUniformsOut,
TOutputVulkanGLSL *outputGLSL);
};
} // namespace sh

Просмотреть файл

@ -83,6 +83,10 @@ angle_source_set("angle_metal_backend") {
"${angle_root}:libANGLE_headers",
]
deps = [
"${angle_spirv_cross_dir}/gn:spirv_cross_sources",
]
objc_flags = [
"-Wno-nullability-completeness",
"-Wno-unguarded-availability",

Просмотреть файл

@ -13,6 +13,8 @@
#include <sstream>
#include <spirv_msl.hpp>
#include "common/debug.h"
#include "libANGLE/Context.h"
#include "libANGLE/ProgramLinkedResources.h"
@ -31,6 +33,85 @@ namespace
#define SHADER_ENTRY_NAME @"main0"
spv::ExecutionModel ShaderTypeToSpvExecutionModel(gl::ShaderType shaderType)
{
switch (shaderType)
{
case gl::ShaderType::Vertex:
return spv::ExecutionModelVertex;
case gl::ShaderType::Fragment:
return spv::ExecutionModelFragment;
default:
UNREACHABLE();
return spv::ExecutionModelMax;
}
}
// Some GLSL variables need 2 binding points in metal. For example,
// glsl sampler will be converted to 2 metal objects: texture and sampler.
// Thus we need to set 2 binding points for one glsl sampler variable.
using BindingField = uint32_t spirv_cross::MSLResourceBinding::*;
template <BindingField bindingField1, BindingField bindingField2 = bindingField1>
angle::Result BindResources(spirv_cross::CompilerMSL *compiler,
const spirv_cross::SmallVector<spirv_cross::Resource> &resources,
gl::ShaderType shaderType)
{
auto &compilerMsl = *compiler;
for (const spirv_cross::Resource &resource : resources)
{
spirv_cross::MSLResourceBinding resBinding;
resBinding.stage = ShaderTypeToSpvExecutionModel(shaderType);
if (compilerMsl.has_decoration(resource.id, spv::DecorationDescriptorSet))
{
resBinding.desc_set =
compilerMsl.get_decoration(resource.id, spv::DecorationDescriptorSet);
}
if (!compilerMsl.has_decoration(resource.id, spv::DecorationBinding))
{
continue;
}
resBinding.binding = compilerMsl.get_decoration(resource.id, spv::DecorationBinding);
uint32_t bindingPoint;
// NOTE(hqle): We use separate discrete binding point for now, in future, we should use
// one argument buffer for each descriptor set.
switch (resBinding.desc_set)
{
case 0:
// Use resBinding.binding as binding point.
bindingPoint = resBinding.binding;
break;
case mtl::kDriverUniformsBindingIndex:
bindingPoint = mtl::kDriverUniformsBindingIndex;
break;
case mtl::kDefaultUniformsBindingIndex:
// NOTE(hqle): Properly handle transform feedbacks and UBO binding once ES 3.0 is
// implemented.
bindingPoint = mtl::kDefaultUniformsBindingIndex;
break;
default:
// We don't support this descriptor set.
continue;
}
// bindingField can be buffer or texture, which will be translated to [[buffer(d)]] or
// [[texture(d)]] or [[sampler(d)]]
resBinding.*bindingField1 = bindingPoint;
if (bindingField1 != bindingField2)
{
resBinding.*bindingField2 = bindingPoint;
}
compilerMsl.add_msl_resource_binding(resBinding);
}
return angle::Result::Continue;
}
void InitDefaultUniformBlock(const std::vector<sh::Uniform> &uniforms,
gl::Shader *shader,
sh::BlockLayoutMap *blockLayoutMapOut,
@ -214,7 +295,7 @@ std::unique_ptr<LinkEvent> ProgramMtl::link(const gl::Context *context,
// assignment done in that function.
linkResources(resources);
mtl::GlslangUtils::GetShaderSource(mState, resources, &mShaderSource);
mtl::GlslangGetShaderSource(mState, resources, &mShaderSource);
// NOTE(hqle): Parallelize linking.
return std::make_unique<LinkEventDone>(linkImpl(context, infoLog));
@ -231,8 +312,8 @@ angle::Result ProgramMtl::linkImpl(const gl::Context *glContext, gl::InfoLog &in
// Convert GLSL to spirv code
gl::ShaderMap<std::vector<uint32_t>> shaderCodes;
ANGLE_TRY(mtl::GlslangUtils::GetShaderCode(contextMtl, contextMtl->getCaps(), false,
mShaderSource, &shaderCodes));
ANGLE_TRY(mtl::GlslangGetShaderSpirvCode(contextMtl, contextMtl->getCaps(), false,
mShaderSource, &shaderCodes));
// Convert spirv code to MSL
ANGLE_TRY(convertToMsl(glContext, gl::ShaderType::Vertex, infoLog,
@ -339,9 +420,47 @@ angle::Result ProgramMtl::convertToMsl(const gl::Context *glContext,
gl::InfoLog &infoLog,
std::vector<uint32_t> *sprivCode)
{
std::string translatedMsl = "TODO";
ContextMtl *contextMtl = mtl::GetImpl(glContext);
UNIMPLEMENTED();
spirv_cross::CompilerMSL compilerMsl(std::move(*sprivCode));
spirv_cross::CompilerMSL::Options compOpt;
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
compOpt.platform = spirv_cross::CompilerMSL::Options::macOS;
#else
compOpt.platform = spirv_cross::CompilerMSL::Options::iOS;
#endif
if (ANGLE_APPLE_AVAILABLE_XCI(10.14, 13.0, 12))
{
// Use Metal 2.1
compOpt.set_msl_version(2, 1);
}
else
{
// Always use at least Metal 2.0.
compOpt.set_msl_version(2);
}
compilerMsl.set_msl_options(compOpt);
// Tell spirv-cross to map default & driver uniform blocks & samplers as we want
spirv_cross::ShaderResources mslRes = compilerMsl.get_shader_resources();
ANGLE_TRY(BindResources<&spirv_cross::MSLResourceBinding::msl_buffer>(
&compilerMsl, mslRes.uniform_buffers, shaderType));
ANGLE_TRY((BindResources<&spirv_cross::MSLResourceBinding::msl_sampler,
&spirv_cross::MSLResourceBinding::msl_texture>(
&compilerMsl, mslRes.sampled_images, shaderType)));
// NOTE(hqle): spirv-cross uses exceptions to report error, what should we do here
// in case of error?
std::string translatedMsl = compilerMsl.compile();
if (translatedMsl.size() == 0)
{
ANGLE_MTL_CHECK(contextMtl, false, GL_INVALID_OPERATION);
}
// Create actual Metal shader
ANGLE_TRY(createMslShader(glContext, shaderType, infoLog, translatedMsl));

Просмотреть файл

@ -18,19 +18,15 @@ namespace rx
{
namespace mtl
{
class GlslangUtils
{
public:
static void GetShaderSource(const gl::ProgramState &programState,
const gl::ProgramLinkedResources &resources,
gl::ShaderMap<std::string> *shaderSourcesOut);
void GlslangGetShaderSource(const gl::ProgramState &programState,
const gl::ProgramLinkedResources &resources,
gl::ShaderMap<std::string> *shaderSourcesOut);
static angle::Result GetShaderCode(ErrorHandler *context,
const gl::Caps &glCaps,
bool enableLineRasterEmulation,
const gl::ShaderMap<std::string> &shaderSources,
gl::ShaderMap<std::vector<uint32_t>> *shaderCodeOut);
};
angle::Result GlslangGetShaderSpirvCode(ErrorHandler *context,
const gl::Caps &glCaps,
bool enableLineRasterEmulation,
const gl::ShaderMap<std::string> &shaderSources,
gl::ShaderMap<std::vector<uint32_t>> *shaderCodeOut);
} // namespace mtl
} // namespace rx

Просмотреть файл

@ -14,24 +14,50 @@ namespace rx
{
namespace mtl
{
// static
void GlslangUtils::GetShaderSource(const gl::ProgramState &programState,
const gl::ProgramLinkedResources &resources,
gl::ShaderMap<std::string> *shaderSourcesOut)
namespace
{
UNIMPLEMENTED();
angle::Result HandleError(ErrorHandler *context, GlslangError)
{
ANGLE_MTL_TRY(context, false);
return angle::Result::Stop;
}
// static
angle::Result GlslangUtils::GetShaderCode(ErrorHandler *context,
const gl::Caps &glCaps,
bool enableLineRasterEmulation,
const gl::ShaderMap<std::string> &shaderSources,
gl::ShaderMap<std::vector<uint32_t>> *shaderCodeOut)
GlslangSourceOptions CreateSourceOptions()
{
UNIMPLEMENTED();
return angle::Result::Stop;
GlslangSourceOptions options;
// We don't actually use descriptor set for now, the actual binding will be done inside
// ProgramMtl using spirv-cross.
options.uniformsAndXfbDescriptorSetIndex = kDefaultUniformsBindingIndex;
options.textureDescriptorSetIndex = 0;
options.driverUniformsDescriptorSetIndex = kDriverUniformsBindingIndex;
// NOTE(hqle): Unused for now, until we support ES 3.0
options.shaderResourceDescriptorSetIndex = -1;
options.xfbBindingIndexStart = -1;
static_assert(kDefaultUniformsBindingIndex != 0, "kDefaultUniformsBindingIndex must not be 0");
static_assert(kDriverUniformsBindingIndex != 0, "kDriverUniformsBindingIndex must not be 0");
return options;
}
} // namespace
void GlslangGetShaderSource(const gl::ProgramState &programState,
const gl::ProgramLinkedResources &resources,
gl::ShaderMap<std::string> *shaderSourcesOut)
{
rx::GlslangGetShaderSource(CreateSourceOptions(), false, programState, resources,
shaderSourcesOut);
}
angle::Result GlslangGetShaderSpirvCode(ErrorHandler *context,
const gl::Caps &glCaps,
bool enableLineRasterEmulation,
const gl::ShaderMap<std::string> &shaderSources,
gl::ShaderMap<std::vector<uint32_t>> *shaderCodeOut)
{
return rx::GlslangGetShaderSpirvCode(
[context](GlslangError error) { return HandleError(context, error); }, glCaps,
enableLineRasterEmulation, shaderSources, shaderCodeOut);
}
} // namespace mtl
} // namespace rx

10
third_party/spirv-cross/README.angle поставляемый Normal file
Просмотреть файл

@ -0,0 +1,10 @@
Name: Khronos SPIRV-Cross
Short Name: spirv-cross
URL: https://github.com/KhronosGroup/SPIRV-Cross
Version: N/A
Security Critical: yes
License: Apache 2.0
License File: LICENSE
Description:
A tool designed for parsing and converting SPIR-V to other shader languages.