MoltenVKShaderConverter tool add MSL version and platform command-line options.
Add SPIRVToMSLConverterOptions::platform to track platform. Default to build platform. Update default SPIRVToMSLConverterOptions MSL version to 2.1. MoltenVKShaderConverter test MSL compilation use same MSL version as conversion. Default min perf tracking value to 0.0. SPIRVToMSLConverter.h reference spirv.hpp via SPIRV-Cross framework. Update What's New document.
This commit is contained in:
parent
6bbd0eca75
commit
eca03b8de1
@ -13,6 +13,17 @@ For best results, use a Markdown reader.*
|
||||
|
||||
|
||||
|
||||
MoltenVK 1.0.35
|
||||
---------------
|
||||
|
||||
Released TBD
|
||||
|
||||
- Don't use setVertexBytes() for passing tessellation vertex counts.
|
||||
- Fix zero local threadgroup size in indirect tessellated rendering.
|
||||
- MoltenVKShaderConverter tool add MSL version and platform command-line options.
|
||||
|
||||
|
||||
|
||||
MoltenVK 1.0.34
|
||||
---------------
|
||||
|
||||
@ -115,7 +126,7 @@ Released 2019/02/28
|
||||
runtime environment variable.
|
||||
Set MSL version for shader compiling from Metal feature set.
|
||||
- Don't warn on identity swizzles when `fullImageViewSwizzle` config setting is enabled.
|
||||
- Track version of spvAux buffer struct in SPRIV-Cross and fail build if different
|
||||
- Track version of spvAux buffer struct in SPIRV-Cross and fail build if different
|
||||
than version expected by MoltenVK.
|
||||
- Add static and dynamic libraries to MoltenVKShaderConverter project.
|
||||
- Fix crash from use of MTLDevice registryID on early OS versions.
|
||||
@ -792,7 +803,7 @@ Released 2018/03/19
|
||||
- Add build and runtime OS and device requirements to documentation.
|
||||
- Add Compliance and Contribution sections to README.md.
|
||||
- Remove executable permissions from non-executable files.
|
||||
- Update to latest SPRIV-Cross.
|
||||
- Update to latest SPIRV-Cross.
|
||||
- Update copyright dates to 2018.
|
||||
|
||||
|
||||
|
@ -1943,7 +1943,9 @@ void MVKDevice::addActivityPerformanceImpl(MVKPerformanceTracker& shaderCompilat
|
||||
lock_guard<mutex> lock(_perfLock);
|
||||
|
||||
double currInterval = mvkGetElapsedMilliseconds(startTime, endTime);
|
||||
shaderCompilationEvent.minimumDuration = min(currInterval, shaderCompilationEvent.minimumDuration);
|
||||
shaderCompilationEvent.minimumDuration = ((shaderCompilationEvent.minimumDuration == 0.0)
|
||||
? currInterval :
|
||||
min(currInterval, shaderCompilationEvent.minimumDuration));
|
||||
shaderCompilationEvent.maximumDuration = max(currInterval, shaderCompilationEvent.maximumDuration);
|
||||
double totalInterval = (shaderCompilationEvent.averageDuration * shaderCompilationEvent.count++) + currInterval;
|
||||
shaderCompilationEvent.averageDuration = totalInterval / shaderCompilationEvent.count;
|
||||
@ -2072,7 +2074,7 @@ void MVKDevice::initPerformanceTracking() {
|
||||
MVKPerformanceTracker initPerf;
|
||||
initPerf.count = 0;
|
||||
initPerf.averageDuration = 0.0;
|
||||
initPerf.minimumDuration = numeric_limits<double>::max();
|
||||
initPerf.minimumDuration = 0.0;
|
||||
initPerf.maximumDuration = 0.0;
|
||||
|
||||
_performanceStatistics.shaderCompilation.hashShaderCode = initPerf;
|
||||
|
@ -84,6 +84,15 @@ MVK_PUBLIC_SYMBOL std::string SPIRVToMSLConverterOptions::printMSLVersion(uint32
|
||||
return verStr;
|
||||
}
|
||||
|
||||
MVK_PUBLIC_SYMBOL mvk::SPIRVToMSLConverterOptions::Platform SPIRVToMSLConverterOptions::getNativePlatform() {
|
||||
#if MVK_MACOS
|
||||
return SPIRVToMSLConverterOptions::macOS;
|
||||
#endif
|
||||
#if MVK_IOS
|
||||
return SPIRVToMSLConverterOptions::iOS;
|
||||
#endif
|
||||
}
|
||||
|
||||
MVK_PUBLIC_SYMBOL bool MSLVertexAttribute::matches(const MSLVertexAttribute& other) const {
|
||||
if (location != other.location) { return false; }
|
||||
if (mslBuffer != other.mslBuffer) { return false; }
|
||||
@ -182,6 +191,9 @@ MVK_PUBLIC_SYMBOL void SPIRVToMSLConverterContext::alignWith(const SPIRVToMSLCon
|
||||
#pragma mark -
|
||||
#pragma mark SPIRVToMSLConverter
|
||||
|
||||
// Return the SPIRV-Cross platform enum corresponding to a SPIRVToMSLConverterOptions platform enum value.
|
||||
SPIRV_CROSS_NAMESPACE::CompilerMSL::Options::Platform getCompilerMSLPlatform(SPIRVToMSLConverterOptions::Platform platform);
|
||||
|
||||
// Populates the entry point with info extracted from the SPRI-V compiler.
|
||||
void populateEntryPoint(SPIRVEntryPoint& entryPoint, SPIRV_CROSS_NAMESPACE::Compiler* pCompiler, SPIRVToMSLConverterOptions& options);
|
||||
|
||||
@ -238,14 +250,7 @@ MVK_PUBLIC_SYMBOL bool SPIRVToMSLConverter::convert(SPIRVToMSLConverterContext&
|
||||
// Establish the MSL options for the compiler
|
||||
// This needs to be done in two steps...for CompilerMSL and its superclass.
|
||||
auto mslOpts = pMSLCompiler->get_msl_options();
|
||||
|
||||
#if MVK_MACOS
|
||||
mslOpts.platform = SPIRV_CROSS_NAMESPACE::CompilerMSL::Options::macOS;
|
||||
#endif
|
||||
#if MVK_IOS
|
||||
mslOpts.platform = SPIRV_CROSS_NAMESPACE::CompilerMSL::Options::iOS;
|
||||
#endif
|
||||
|
||||
mslOpts.platform = getCompilerMSLPlatform(context.options.platform);
|
||||
mslOpts.msl_version = context.options.mslVersion;
|
||||
mslOpts.texel_buffer_texture_width = context.options.texelBufferTextureWidth;
|
||||
mslOpts.aux_buffer_index = context.options.auxBufferIndex;
|
||||
@ -441,6 +446,14 @@ void SPIRVToMSLConverter::logSource(string& src, const char* srcLang, const char
|
||||
|
||||
#pragma mark Support functions
|
||||
|
||||
// Return the SPIRV-Cross platform enum corresponding to a SPIRVToMSLConverterOptions platform enum value.
|
||||
SPIRV_CROSS_NAMESPACE::CompilerMSL::Options::Platform getCompilerMSLPlatform(SPIRVToMSLConverterOptions::Platform platform) {
|
||||
switch (platform) {
|
||||
case SPIRVToMSLConverterOptions::macOS: return SPIRV_CROSS_NAMESPACE::CompilerMSL::Options::macOS;
|
||||
case SPIRVToMSLConverterOptions::iOS: return SPIRV_CROSS_NAMESPACE::CompilerMSL::Options::iOS;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate a workgroup size dimension.
|
||||
void populateWorkgroupDimension(SPIRVWorkgroupSizeDimension& wgDim, uint32_t size, SPIRV_CROSS_NAMESPACE::SpecializationConstant& spvSpecConst) {
|
||||
wgDim.size = max(size, 1u);
|
||||
|
@ -19,7 +19,7 @@
|
||||
#ifndef __SPIRVToMSLConverter_h_
|
||||
#define __SPIRVToMSLConverter_h_ 1
|
||||
|
||||
#include "../SPIRV-Cross/spirv.hpp"
|
||||
#include <SPIRV-Cross/spirv.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
@ -32,11 +32,18 @@ namespace mvk {
|
||||
|
||||
/** Options for converting SPIR-V to Metal Shading Language */
|
||||
typedef struct SPIRVToMSLConverterOptions {
|
||||
|
||||
enum Platform {
|
||||
iOS = 0,
|
||||
macOS = 1
|
||||
};
|
||||
|
||||
std::string entryPointName;
|
||||
spv::ExecutionModel entryPointStage = spv::ExecutionModelMax;
|
||||
spv::ExecutionMode tessPatchKind = spv::ExecutionModeMax;
|
||||
|
||||
uint32_t mslVersion = makeMSLVersion(2);
|
||||
uint32_t mslVersion = makeMSLVersion(2, 1);
|
||||
Platform platform = getNativePlatform();
|
||||
uint32_t texelBufferTextureWidth = 4096;
|
||||
uint32_t auxBufferIndex = 0;
|
||||
uint32_t indirectParamsBufferIndex = 0;
|
||||
@ -81,6 +88,8 @@ namespace mvk {
|
||||
|
||||
static std::string printMSLVersion(uint32_t mslVersion, bool includePatch = false);
|
||||
|
||||
static Platform getNativePlatform();
|
||||
|
||||
} SPIRVToMSLConverterOptions;
|
||||
|
||||
/**
|
||||
|
@ -920,6 +920,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)\"",
|
||||
"\"$(SRCROOT)/glslang/External/spirv-tools/include\"",
|
||||
"\"$(SRCROOT)/glslang/External/spirv-tools/external/spirv-headers/include\"",
|
||||
);
|
||||
@ -973,6 +974,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)\"",
|
||||
"\"$(SRCROOT)/glslang/External/spirv-tools/include\"",
|
||||
"\"$(SRCROOT)/glslang/External/spirv-tools/external/spirv-headers/include\"",
|
||||
);
|
||||
|
@ -103,6 +103,22 @@
|
||||
argument = "-mo"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-mv"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "2.1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-mp"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "ios"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-XS"
|
||||
isEnabled = "NO">
|
||||
|
@ -45,7 +45,7 @@ uint64_t MVKPerformanceTracker::getTimestamp() { return mvkGetTimestamp(); }
|
||||
|
||||
void MVKPerformanceTracker::accumulate(uint64_t startTime, uint64_t endTime) {
|
||||
double currInterval = mvkGetElapsedMilliseconds(startTime, endTime);
|
||||
minimumDuration = min(currInterval, minimumDuration);
|
||||
minimumDuration = (minimumDuration == 0.0) ? currInterval : min(currInterval, minimumDuration);
|
||||
maximumDuration = max(currInterval, maximumDuration);
|
||||
double totalInterval = (averageDuration * count++) + currInterval;
|
||||
averageDuration = totalInterval / count;
|
||||
@ -57,6 +57,8 @@ void MVKPerformanceTracker::accumulate(uint64_t startTime, uint64_t endTime) {
|
||||
|
||||
|
||||
int MoltenVKShaderConverterTool::run() {
|
||||
if ( !_isActive ) { return EXIT_FAILURE; }
|
||||
|
||||
bool success = false;
|
||||
if ( !_directoryPath.empty() ) {
|
||||
string errMsg;
|
||||
@ -203,6 +205,8 @@ bool MoltenVKShaderConverterTool::convertSPIRV(const vector<uint32_t>& spv,
|
||||
|
||||
// Derive the context under which conversion will occur
|
||||
SPIRVToMSLConverterContext mslContext;
|
||||
mslContext.options.platform = _mslPlatform;
|
||||
mslContext.options.setMSLVersion(_mslVersionMajor, _mslVersionMinor, _mslVersionPatch);
|
||||
mslContext.options.shouldFlipVertexY = _shouldFlipVertexY;
|
||||
|
||||
SPIRVToMSLConverter spvConverter;
|
||||
@ -227,9 +231,9 @@ bool MoltenVKShaderConverterTool::convertSPIRV(const vector<uint32_t>& spv,
|
||||
const string& msl = spvConverter.getMSL();
|
||||
|
||||
string compileErrMsg;
|
||||
bool wasCompiled = compile(msl, compileErrMsg);
|
||||
bool wasCompiled = compile(msl, compileErrMsg, _mslVersionMajor, _mslVersionMinor, _mslVersionPatch);
|
||||
if (compileErrMsg.size() > 0) {
|
||||
string preamble = wasCompiled ? "is valid but the validation compilation produced warnings " : "failed a validation compilation ";
|
||||
string preamble = wasCompiled ? "is valid but the validation compilation produced warnings: " : "failed a validation compilation: ";
|
||||
compileErrMsg = "Generated MSL " + preamble + compileErrMsg;
|
||||
log(compileErrMsg.c_str());
|
||||
} else {
|
||||
@ -305,6 +309,12 @@ void MoltenVKShaderConverterTool::showUsage() {
|
||||
log(" The optional path parameter specifies the path to a single");
|
||||
log(" file to contain the MSL code. When using the -d option,");
|
||||
log(" the path parameter is ignored.");
|
||||
log(" -mv mslVersion - MSL version to output.");
|
||||
log(" Must be in form n[.n][.n] (eg. 2, 2.1, or 2.1.0).");
|
||||
log(" Defaults to the most recent MSL version for the platform");
|
||||
log(" on which this tool is executed.");
|
||||
log(" -mp mslPlatform - MSL platform. Must be one of macos or ios.");
|
||||
log(" Defaults to the platform on which this tool is executed (macos).");
|
||||
log(" -t shaderType - Shader type: vertex or fragment. Must be one of v, f, or c.");
|
||||
log(" May be omitted to auto-detect.");
|
||||
log(" -c - Combine the GLSL and converted Metal Shader source code");
|
||||
@ -363,7 +373,6 @@ MoltenVKShaderConverterTool::MoltenVKShaderConverterTool(int argc, const char* a
|
||||
extractTokens(_defaultSPIRVShaderExtns, _spvFileExtns);
|
||||
_origPathExtnSep = "_";
|
||||
_shaderStage = kMVKShaderStageAuto;
|
||||
_isActive = false;
|
||||
_shouldUseDirectoryRecursion = false;
|
||||
_shouldReadGLSL = false;
|
||||
_shouldReadSPIRV = false;
|
||||
@ -375,6 +384,11 @@ MoltenVKShaderConverterTool::MoltenVKShaderConverterTool(int argc, const char* a
|
||||
_shouldLogConversions = false;
|
||||
_shouldReportPerformance = false;
|
||||
|
||||
_mslVersionMajor = 2;
|
||||
_mslVersionMinor = 1;
|
||||
_mslVersionPatch = 0;
|
||||
_mslPlatform = SPIRVToMSLConverterOptions::getNativePlatform();
|
||||
|
||||
_isActive = parseArgs(argc, argv);
|
||||
if ( !_isActive ) { showUsage(); }
|
||||
}
|
||||
@ -427,6 +441,39 @@ bool MoltenVKShaderConverterTool::parseArgs(int argc, const char* argv[]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (equal(arg, "-mv", true)) {
|
||||
int optIdx = argIdx;
|
||||
string mslVerStr;
|
||||
argIdx = optionalParam(mslVerStr, argIdx, argc, argv);
|
||||
if (argIdx == optIdx || mslVerStr.length() == 0) { return false; }
|
||||
vector<uint32_t> mslVerTokens;
|
||||
extractTokens(mslVerStr, mslVerTokens);
|
||||
auto tknCnt = mslVerTokens.size();
|
||||
_mslVersionMajor = (tknCnt > 0) ? mslVerTokens[0] : 0;
|
||||
_mslVersionMinor = (tknCnt > 1) ? mslVerTokens[1] : 0;
|
||||
_mslVersionPatch = (tknCnt > 2) ? mslVerTokens[2] : 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (equal(arg, "-mp", true)) {
|
||||
int optIdx = argIdx;
|
||||
string shdrTypeStr;
|
||||
argIdx = optionalParam(shdrTypeStr, argIdx, argc, argv);
|
||||
if (argIdx == optIdx || shdrTypeStr.length() == 0) { return false; }
|
||||
|
||||
switch (shdrTypeStr.front()) {
|
||||
case 'm':
|
||||
_mslPlatform = SPIRVToMSLConverterOptions::macOS;
|
||||
break;
|
||||
case 'i':
|
||||
_mslPlatform = SPIRVToMSLConverterOptions::iOS;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (equal(arg, "-t", true)) {
|
||||
int optIdx = argIdx;
|
||||
string shdrTypeStr;
|
||||
@ -444,7 +491,7 @@ bool MoltenVKShaderConverterTool::parseArgs(int argc, const char* argv[]) {
|
||||
_shaderStage = kMVKShaderStageCompute;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@ -577,6 +624,14 @@ void mvk::extractTokens(string str, vector<string>& tokens) {
|
||||
split(tokens, str, " \t\n\f", false);
|
||||
}
|
||||
|
||||
void mvk::extractTokens(string str, vector<uint32_t>& tokens) {
|
||||
vector<string> stringTokens;
|
||||
split(stringTokens, str, ".", false);
|
||||
for (auto& st : stringTokens) {
|
||||
tokens.push_back((uint32_t)strtol(st.c_str(), nullptr, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Compares the specified characters ignoring case.
|
||||
static bool compareIgnoringCase(unsigned char a, unsigned char b) {
|
||||
return tolower(a) == tolower(b);
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
|
||||
#include "GLSLConversion.h"
|
||||
#include "SPIRVToMSLConverter.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@ -28,9 +29,9 @@ namespace mvk {
|
||||
|
||||
typedef struct {
|
||||
uint32_t count = 0;
|
||||
double averageDuration = 0;
|
||||
double minimumDuration = std::numeric_limits<double>::max();
|
||||
double maximumDuration = 0;
|
||||
double averageDuration = 0.0;
|
||||
double minimumDuration = 0.0;
|
||||
double maximumDuration = 0.0;
|
||||
|
||||
uint64_t getTimestamp();
|
||||
void accumulate(uint64_t startTime, uint64_t endTime = 0);
|
||||
@ -102,6 +103,10 @@ namespace mvk {
|
||||
MVKShaderStage _shaderStage;
|
||||
MVKPerformanceTracker _glslConversionPerformance;
|
||||
MVKPerformanceTracker _spvConversionPerformance;
|
||||
uint32_t _mslVersionMajor;
|
||||
uint32_t _mslVersionMinor;
|
||||
uint32_t _mslVersionPatch;
|
||||
SPIRVToMSLConverterOptions::Platform _mslPlatform;
|
||||
bool _isActive;
|
||||
bool _shouldUseDirectoryRecursion;
|
||||
bool _shouldReadGLSL;
|
||||
@ -121,10 +126,16 @@ namespace mvk {
|
||||
|
||||
/**
|
||||
* Extracts whitespace-delimited tokens from the specified string and
|
||||
* appends them to the specified vector. The vector is not cleared first.
|
||||
* appends them to the specified vector. The vector is cleared first.
|
||||
*/
|
||||
void extractTokens(std::string str, std::vector<std::string>& tokens);
|
||||
|
||||
/**
|
||||
* Extracts period-delimited tokens from the specified string and
|
||||
* appends them to the specified vector. The vector is cleared first.
|
||||
*/
|
||||
void extractTokens(std::string str, std::vector<uint32_t>& tokens);
|
||||
|
||||
/** Compares the specified strings, with or without sensitivity to case. */
|
||||
bool equal(std::string const& a, std::string const& b, bool checkCase = true);
|
||||
|
||||
|
@ -48,6 +48,10 @@ namespace mvk {
|
||||
* If unsuccessful, the return value will be false and the errMsg will contain an
|
||||
* error message. Otherwise the return value will be true and the errMsg will be empty.
|
||||
*/
|
||||
bool compile(const std::string& mslSourceCode, std::string& errMsg);
|
||||
bool compile(const std::string& mslSourceCode,
|
||||
std::string& errMsg,
|
||||
uint32_t mslVersionMajor,
|
||||
uint32_t mslVersionMinor = 0,
|
||||
uint32_t mslVersionPoint = 0);
|
||||
|
||||
}
|
||||
|
@ -55,20 +55,49 @@ bool mvk::iterateDirectory(const string& dirPath,
|
||||
return success;
|
||||
}
|
||||
|
||||
/** Concrete template implementation to allow MoltenVKShaderConverterTool to iterate the files in a directory. */
|
||||
// Concrete template implementation to allow MoltenVKShaderConverterTool to iterate the files in a directory.
|
||||
template bool mvk::iterateDirectory<MoltenVKShaderConverterTool>(const string& dirPath,
|
||||
MoltenVKShaderConverterTool& fileProcessor,
|
||||
bool isRecursive,
|
||||
string& errMsg);
|
||||
|
||||
bool mvk::compile(const string& mslSourceCode, string& errMsg) {
|
||||
bool mvk::compile(const string& mslSourceCode,
|
||||
string& errMsg,
|
||||
uint32_t mslVersionMajor,
|
||||
uint32_t mslVersionMinor,
|
||||
uint32_t mslVersionPoint) {
|
||||
|
||||
#define mslVer(MJ, MN, PT) mslVersionMajor == MJ && mslVersionMinor == MN && mslVersionPoint == PT
|
||||
|
||||
MTLLanguageVersion mslVerEnum = (MTLLanguageVersion)0;
|
||||
if (mslVer(2, 1, 0)) {
|
||||
if (@available(macOS 10.14, *)) {
|
||||
mslVerEnum = MTLLanguageVersion2_1;
|
||||
}
|
||||
} else if (mslVer(2, 0, 0)) {
|
||||
if (@available(macOS 10.13, *)) {
|
||||
mslVerEnum = MTLLanguageVersion2_0;
|
||||
}
|
||||
} else if (mslVer(1, 2, 0)) {
|
||||
mslVerEnum = MTLLanguageVersion1_2;
|
||||
} else if (mslVer(1, 1, 0)) {
|
||||
mslVerEnum = MTLLanguageVersion1_1;
|
||||
}
|
||||
|
||||
if ( !mslVerEnum ) {
|
||||
errMsg = [NSString stringWithFormat: @"%d.%d.%d is not a valid MSL version number on this device",
|
||||
mslVersionMajor, mslVersionMinor, mslVersionPoint].UTF8String;
|
||||
return false;
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
MTLCompileOptions* mtlCompileOptions = [[MTLCompileOptions new] autorelease];
|
||||
mtlCompileOptions.languageVersion = mslVerEnum;
|
||||
NSError* err = nil;
|
||||
id<MTLLibrary> mtlLib = [[MTLCreateSystemDefaultDevice() newLibraryWithSource: @(mslSourceCode.c_str())
|
||||
options: [[MTLCompileOptions new] autorelease]
|
||||
options: mtlCompileOptions
|
||||
error: &err] autorelease];
|
||||
errMsg = err ? [NSString stringWithFormat: @"(Error code %li):\n%@", (long)err.code, err.localizedDescription].UTF8String
|
||||
: "";
|
||||
errMsg = err ? [NSString stringWithFormat: @"(Error code %li):\n%@", (long)err.code, err.localizedDescription].UTF8String : "";
|
||||
return !!mtlLib;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user