blob: 41d36014769521b40d3fe1ea58274e13c6cd3e75 [file] [log] [blame]
/*
* SPIRVToMSLConverter.cpp
*
* Copyright (c) 2015-2022 The Brenwill Workshop Ltd. (http://www.brenwill.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SPIRVToMSLConverter.h"
#include "MVKCommonEnvironment.h"
#include "MVKStrings.h"
#include "FileSupport.h"
#include "SPIRVSupport.h"
#include <fstream>
using namespace mvk;
using namespace std;
using namespace spv;
using namespace SPIRV_CROSS_NAMESPACE;
#pragma mark -
#pragma mark SPIRVToMSLConversionConfiguration
// Returns whether the container contains an item equal to the value.
template<class C, class T>
bool contains(const C& container, const T& val) {
for (const T& cVal : container) { if (cVal == val) { return true; } }
return false;
}
// Returns whether the vector contains the value (using a matches(T&) comparison member function). */
template<class T>
bool containsMatching(const vector<T>& vec, const T& val) {
for (const T& vecVal : vec) { if (vecVal.matches(val)) { return true; } }
return false;
}
MVK_PUBLIC_SYMBOL bool SPIRVToMSLConversionOptions::matches(const SPIRVToMSLConversionOptions& other) const {
if (memcmp(&mslOptions, &other.mslOptions, sizeof(mslOptions)) != 0) { return false; }
if (entryPointStage != other.entryPointStage) { return false; }
if (entryPointName != other.entryPointName) { return false; }
if (tessPatchKind != other.tessPatchKind) { return false; }
if (numTessControlPoints != other.numTessControlPoints) { return false; }
if (shouldFlipVertexY != other.shouldFlipVertexY) { return false; }
return true;
}
MVK_PUBLIC_SYMBOL string SPIRVToMSLConversionOptions::printMSLVersion(uint32_t mslVersion, bool includePatch) {
string verStr;
uint32_t major = mslVersion / 10000;
verStr += to_string(major);
uint32_t minor = (mslVersion - CompilerMSL::Options::make_msl_version(major)) / 100;
verStr += ".";
verStr += to_string(minor);
if (includePatch) {
uint32_t patch = mslVersion - CompilerMSL::Options::make_msl_version(major, minor);
verStr += ".";
verStr += to_string(patch);
}
return verStr;
}
MVK_PUBLIC_SYMBOL SPIRVToMSLConversionOptions::SPIRVToMSLConversionOptions() {
// Explicitly set mslOptions to defaults over cleared memory to ensure all instances
// have exactly the same memory layout when using memory comparison in matches().
memset(&mslOptions, 0, sizeof(mslOptions));
mslOptions = CompilerMSL::Options();
#if MVK_MACOS
mslOptions.platform = CompilerMSL::Options::macOS;
#endif
#if MVK_IOS
mslOptions.platform = CompilerMSL::Options::iOS;
#endif
#if MVK_TVOS
mslOptions.platform = CompilerMSL::Options::iOS;
#endif
mslOptions.pad_fragment_output_components = true;
}
MVK_PUBLIC_SYMBOL bool mvk::MSLShaderInput::matches(const mvk::MSLShaderInput& other) const {
if (memcmp(&shaderInput, &other.shaderInput, sizeof(shaderInput)) != 0) { return false; }
if (binding != other.binding) { return false; }
return true;
}
MVK_PUBLIC_SYMBOL mvk::MSLShaderInput::MSLShaderInput() {
// Explicitly set shaderInput to defaults over cleared memory to ensure all instances
// have exactly the same memory layout when using memory comparison in matches().
memset(&shaderInput, 0, sizeof(shaderInput));
shaderInput = SPIRV_CROSS_NAMESPACE::MSLShaderInput();
}
// If requiresConstExprSampler is false, constExprSampler can be ignored
MVK_PUBLIC_SYMBOL bool mvk::MSLResourceBinding::matches(const MSLResourceBinding& other) const {
if (memcmp(&resourceBinding, &other.resourceBinding, sizeof(resourceBinding)) != 0) { return false; }
if (requiresConstExprSampler != other.requiresConstExprSampler) { return false; }
if (requiresConstExprSampler) {
if (memcmp(&constExprSampler, &other.constExprSampler, sizeof(constExprSampler)) != 0) { return false; }
}
return true;
}
MVK_PUBLIC_SYMBOL mvk::MSLResourceBinding::MSLResourceBinding() {
// Explicitly set resourceBinding and constExprSampler to defaults over cleared memory to ensure
// all instances have exactly the same memory layout when using memory comparison in matches().
memset(&resourceBinding, 0, sizeof(resourceBinding));
resourceBinding = SPIRV_CROSS_NAMESPACE::MSLResourceBinding();
memset(&constExprSampler, 0, sizeof(constExprSampler));
constExprSampler = SPIRV_CROSS_NAMESPACE::MSLConstexprSampler();
}
MVK_PUBLIC_SYMBOL bool mvk::DescriptorBinding::matches(const mvk::DescriptorBinding& other) const {
if (stage != other.stage) { return false; }
if (descriptorSet != other.descriptorSet) { return false; }
if (binding != other.binding) { return false; }
if (index != other.index) { return false; }
return true;
}
MVK_PUBLIC_SYMBOL bool SPIRVToMSLConversionConfiguration::stageSupportsVertexAttributes() const {
return (options.entryPointStage == ExecutionModelVertex ||
options.entryPointStage == ExecutionModelTessellationControl ||
options.entryPointStage == ExecutionModelTessellationEvaluation);
}
// Check them all in case inactive VA's duplicate locations used by active VA's.
MVK_PUBLIC_SYMBOL bool SPIRVToMSLConversionConfiguration::isShaderInputLocationUsed(uint32_t location) const {
for (auto& si : shaderInputs) {
if ((si.shaderInput.location == location) && si.outIsUsedByShader) { return true; }
}
return false;
}
MVK_PUBLIC_SYMBOL uint32_t SPIRVToMSLConversionConfiguration::countShaderInputsAt(uint32_t binding) const {
uint32_t siCnt = 0;
for (auto& si : shaderInputs) {
if ((si.binding == binding) && si.outIsUsedByShader) { siCnt++; }
}
return siCnt;
}
MVK_PUBLIC_SYMBOL bool SPIRVToMSLConversionConfiguration::isResourceUsed(ExecutionModel stage, uint32_t descSet, uint32_t binding) const {
for (auto& rb : resourceBindings) {
auto& rbb = rb.resourceBinding;
if (rbb.stage == stage && rbb.desc_set == descSet && rbb.binding == binding) {
return rb.outIsUsedByShader;
}
}
return false;
}
MVK_PUBLIC_SYMBOL void SPIRVToMSLConversionConfiguration::markAllInputsAndResourcesUsed() {
for (auto& si : shaderInputs) { si.outIsUsedByShader = true; }
for (auto& rb : resourceBindings) { rb.outIsUsedByShader = true; }
}
// A single SPIRVToMSLConversionConfiguration instance is used for all pipeline shader stages,
// and the resources can be spread across these shader stages. To improve cache hits when using
// this function to find a cached shader for a particular shader stage, only consider the resources
// that are used in that shader stage. By contrast, discreteDescriptorSet apply across all stages,
// and shaderInputs are populated before each stage, so neither needs to be filtered by stage here.
MVK_PUBLIC_SYMBOL bool SPIRVToMSLConversionConfiguration::matches(const SPIRVToMSLConversionConfiguration& other) const {
if ( !options.matches(other.options) ) { return false; }
for (const auto& si : shaderInputs) {
if (si.outIsUsedByShader && !containsMatching(other.shaderInputs, si)) { return false; }
}
for (const auto& rb : resourceBindings) {
if (rb.resourceBinding.stage == options.entryPointStage &&
rb.outIsUsedByShader &&
!containsMatching(other.resourceBindings, rb)) { return false; }
}
for (const auto& db : dynamicBufferDescriptors) {
if (db.stage == options.entryPointStage &&
!containsMatching(other.dynamicBufferDescriptors, db)) { return false; }
}
for (uint32_t dsIdx : discreteDescriptorSets) {
if ( !contains(other.discreteDescriptorSets, dsIdx)) { return false; }
}
return true;
}
MVK_PUBLIC_SYMBOL void SPIRVToMSLConversionConfiguration::alignWith(const SPIRVToMSLConversionConfiguration& srcContext) {
for (auto& si : shaderInputs) {
si.outIsUsedByShader = false;
for (auto& srcSI : srcContext.shaderInputs) {
if (si.matches(srcSI)) { si.outIsUsedByShader = srcSI.outIsUsedByShader; }
}
}
for (auto& rb : resourceBindings) {
rb.outIsUsedByShader = false;
for (auto& srcRB : srcContext.resourceBindings) {
if (rb.matches(srcRB)) {
rb.outIsUsedByShader = srcRB.outIsUsedByShader;
}
}
}
}
#pragma mark -
#pragma mark SPIRVToMSLConverter
MVK_PUBLIC_SYMBOL void SPIRVToMSLConverter::setSPIRV(const uint32_t* spirvCode, size_t length) {
_spirv.clear(); // Clear for reuse
_spirv.reserve(length);
for (size_t i = 0; i < length; i++) {
_spirv.push_back(spirvCode[i]);
}
}
MVK_PUBLIC_SYMBOL bool SPIRVToMSLConverter::convert(SPIRVToMSLConversionConfiguration& shaderConfig,
bool shouldLogSPIRV,
bool shouldLogMSL,
bool shouldLogGLSL) {
// Uncomment to write SPIR-V to file as a debugging aid
// ofstream spvFile("spirv.spv", ios::binary);
// spvFile.write((char*)_spirv.data(), _spirv.size() << 2);
// spvFile.close();
_wasConverted = true;
_resultLog.clear();
_msl.clear();
_shaderConversionResults.reset();
if (shouldLogSPIRV) { logSPIRV("Converting"); }
CompilerMSL* pMSLCompiler = nullptr;
#ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS
try {
#endif
pMSLCompiler = new CompilerMSL(_spirv);
if (shaderConfig.options.hasEntryPoint()) {
pMSLCompiler->set_entry_point(shaderConfig.options.entryPointName, shaderConfig.options.entryPointStage);
}
// Set up tessellation parameters if needed.
if (shaderConfig.options.entryPointStage == ExecutionModelTessellationControl ||
shaderConfig.options.entryPointStage == ExecutionModelTessellationEvaluation) {
if (shaderConfig.options.tessPatchKind != ExecutionModeMax) {
pMSLCompiler->set_execution_mode(shaderConfig.options.tessPatchKind);
}
if (shaderConfig.options.numTessControlPoints != 0) {
pMSLCompiler->set_execution_mode(ExecutionModeOutputVertices, shaderConfig.options.numTessControlPoints);
}
}
// Establish the MSL options for the compiler
// This needs to be done in two steps...for CompilerMSL and its superclass.
pMSLCompiler->set_msl_options(shaderConfig.options.mslOptions);
auto scOpts = pMSLCompiler->get_common_options();
scOpts.vertex.flip_vert_y = shaderConfig.options.shouldFlipVertexY;
pMSLCompiler->set_common_options(scOpts);
// Add shader inputs
for (auto& si : shaderConfig.shaderInputs) {
pMSLCompiler->add_msl_shader_input(si.shaderInput);
}
// Add resource bindings and hardcoded constexpr samplers
for (auto& rb : shaderConfig.resourceBindings) {
auto& rbb = rb.resourceBinding;
pMSLCompiler->add_msl_resource_binding(rbb);
if (rb.requiresConstExprSampler) {
pMSLCompiler->remap_constexpr_sampler_by_binding(rbb.desc_set, rbb.binding, rb.constExprSampler);
}
}
// Add any descriptor sets that are not using Metal argument buffers.
// This only has an effect if SPIRVToMSLConversionConfiguration::options::mslOptions::argument_buffers is enabled.
for (uint32_t dsIdx : shaderConfig.discreteDescriptorSets) {
pMSLCompiler->add_discrete_descriptor_set(dsIdx);
}
// Add any dynamic buffer bindings.
// This only has an applies if SPIRVToMSLConversionConfiguration::options::mslOptions::argument_buffers is enabled.
if (shaderConfig.options.mslOptions.argument_buffers) {
for (auto& db : shaderConfig.dynamicBufferDescriptors) {
if (db.stage == shaderConfig.options.entryPointStage) {
pMSLCompiler->add_dynamic_buffer(db.descriptorSet, db.binding, db.index);
}
}
}
_msl = pMSLCompiler->compile();
if (shouldLogMSL) { logSource(_msl, "MSL", "Converted"); }
#ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS
} catch (CompilerError& ex) {
string errMsg("MSL conversion error: ");
errMsg += ex.what();
logError(errMsg.data());
if (shouldLogMSL && pMSLCompiler) {
_msl = pMSLCompiler->get_partial_source();
logSource(_msl, "MSL", "Partially converted");
}
}
#endif
// Populate the shader conversion results with info from the compilation run,
// and mark which vertex attributes and resource bindings are used by the shader
populateEntryPoint(pMSLCompiler, shaderConfig.options);
_shaderConversionResults.isRasterizationDisabled = pMSLCompiler && pMSLCompiler->get_is_rasterization_disabled();
_shaderConversionResults.isPositionInvariant = pMSLCompiler && pMSLCompiler->is_position_invariant();
_shaderConversionResults.needsSwizzleBuffer = pMSLCompiler && pMSLCompiler->needs_swizzle_buffer();
_shaderConversionResults.needsOutputBuffer = pMSLCompiler && pMSLCompiler->needs_output_buffer();
_shaderConversionResults.needsPatchOutputBuffer = pMSLCompiler && pMSLCompiler->needs_patch_output_buffer();
_shaderConversionResults.needsBufferSizeBuffer = pMSLCompiler && pMSLCompiler->needs_buffer_size_buffer();
_shaderConversionResults.needsInputThreadgroupMem = pMSLCompiler && pMSLCompiler->needs_input_threadgroup_mem();
_shaderConversionResults.needsDispatchBaseBuffer = pMSLCompiler && pMSLCompiler->needs_dispatch_base_buffer();
_shaderConversionResults.needsViewRangeBuffer = pMSLCompiler && pMSLCompiler->needs_view_mask_buffer();
// When using Metal argument buffers, if the shader is provided with dynamic buffer offsets,
// then it needs a buffer to hold these dynamic offsets.
_shaderConversionResults.needsDynamicOffsetBuffer = false;
if (shaderConfig.options.mslOptions.argument_buffers) {
for (auto& db : shaderConfig.dynamicBufferDescriptors) {
if (db.stage == shaderConfig.options.entryPointStage) {
_shaderConversionResults.needsDynamicOffsetBuffer = true;
}
}
}
for (auto& ctxSI : shaderConfig.shaderInputs) {
ctxSI.outIsUsedByShader = pMSLCompiler->is_msl_shader_input_used(ctxSI.shaderInput.location);
}
for (auto& ctxRB : shaderConfig.resourceBindings) {
if (ctxRB.resourceBinding.stage == shaderConfig.options.entryPointStage) {
ctxRB.outIsUsedByShader = pMSLCompiler->is_msl_resource_binding_used(ctxRB.resourceBinding.stage,
ctxRB.resourceBinding.desc_set,
ctxRB.resourceBinding.binding);
}
}
delete pMSLCompiler;
// To check GLSL conversion
if (shouldLogGLSL) {
CompilerGLSL* pGLSLCompiler = nullptr;
#ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS
try {
#endif
pGLSLCompiler = new CompilerGLSL(_spirv);
auto options = pGLSLCompiler->get_common_options();
options.vulkan_semantics = true;
options.separate_shader_objects = true;
pGLSLCompiler->set_common_options(options);
string glsl = pGLSLCompiler->compile();
logSource(glsl, "GLSL", "Estimated original");
#ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS
} catch (CompilerError& ex) {
string errMsg("Original GLSL extraction error: ");
errMsg += ex.what();
logMsg(errMsg.data());
if (pGLSLCompiler) {
string glsl = pGLSLCompiler->get_partial_source();
logSource(glsl, "GLSL", "Partially converted");
}
}
#endif
delete pGLSLCompiler;
}
return _wasConverted;
}
// Appends the message text to the result log.
void SPIRVToMSLConverter::logMsg(const char* logMsg) {
string trimMsg = trim(logMsg);
if ( !trimMsg.empty() ) {
_resultLog += trimMsg;
_resultLog += "\n\n";
}
}
// Appends the error text to the result log, sets the wasConverted property to false, and returns it.
bool SPIRVToMSLConverter::logError(const char* errMsg) {
logMsg(errMsg);
_wasConverted = false;
return _wasConverted;
}
// Appends the SPIR-V to the result log, indicating whether it is being converted or was converted.
void SPIRVToMSLConverter::logSPIRV(const char* opDesc) {
string spvLog;
mvk::logSPIRV(_spirv, spvLog);
_resultLog += opDesc;
_resultLog += " SPIR-V:\n";
_resultLog += spvLog;
_resultLog += "\nEnd SPIR-V\n\n";
// Uncomment one or both of the following lines to get additional debugging and tracability capabilities.
// The SPIR-V can be written in binary form to a file, and/or logged in human readable form to the console.
// These can be helpful if errors occur during conversion of SPIR-V to MSL.
// writeSPIRVToFile("spvout.spv");
// printf("\n%s\n", getResultLog().c_str());
}
// Writes the SPIR-V code to a file. This can be useful for debugging
// when the SPRIR-V did not originally come from a known file
void SPIRVToMSLConverter::writeSPIRVToFile(string spvFilepath) {
vector<char> fileContents;
spirvToBytes(_spirv, fileContents);
string errMsg;
if (writeFile(spvFilepath, fileContents, errMsg)) {
_resultLog += "Saved SPIR-V to file: " + absolutePath(spvFilepath) + "\n\n";
} else {
_resultLog += "Could not write SPIR-V file. " + errMsg + "\n\n";
}
}
// Validates that the SPIR-V code will disassemble during logging.
bool SPIRVToMSLConverter::validateSPIRV() {
if (_spirv.size() < 5) { return false; }
if (_spirv[0] != MagicNumber) { return false; }
if (_spirv[4] != 0) { return false; }
return true;
}
// Appends the source to the result log, prepending with the operation.
void SPIRVToMSLConverter::logSource(string& src, const char* srcLang, const char* opDesc) {
_resultLog += opDesc;
_resultLog += " ";
_resultLog += srcLang;
_resultLog += ":\n";
_resultLog += src;
_resultLog += "\nEnd ";
_resultLog += srcLang;
_resultLog += "\n\n";
}
void SPIRVToMSLConverter::populateWorkgroupDimension(SPIRVWorkgroupSizeDimension& wgDim,
uint32_t size,
SpecializationConstant& spvSpecConst) {
wgDim.size = max(size, 1u);
wgDim.isSpecialized = (uint32_t(spvSpecConst.id) != 0);
wgDim.specializationID = spvSpecConst.constant_id;
}
// Populates the entry point with info extracted from the SPRI-V compiler.
void SPIRVToMSLConverter::populateEntryPoint(Compiler* pCompiler,
SPIRVToMSLConversionOptions& options) {
if ( !pCompiler ) { return; }
SPIREntryPoint spvEP;
if (options.hasEntryPoint()) {
spvEP = pCompiler->get_entry_point(options.entryPointName, options.entryPointStage);
} else {
const auto& entryPoints = pCompiler->get_entry_points_and_stages();
if ( !entryPoints.empty() ) {
auto& ep = entryPoints[0];
spvEP = pCompiler->get_entry_point(ep.name, ep.execution_model);
}
}
auto& ep = _shaderConversionResults.entryPoint;
ep.mtlFunctionName = spvEP.name;
ep.supportsFastMath = !spvEP.flags.get(ExecutionModeSignedZeroInfNanPreserve);
SpecializationConstant widthSC, heightSC, depthSC;
pCompiler->get_work_group_size_specialization_constants(widthSC, heightSC, depthSC);
auto& wgSize = ep.workgroupSize;
populateWorkgroupDimension(wgSize.width, spvEP.workgroup_size.x, widthSC);
populateWorkgroupDimension(wgSize.height, spvEP.workgroup_size.y, heightSC);
populateWorkgroupDimension(wgSize.depth, spvEP.workgroup_size.z, depthSC);
}