| // Copyright 2013 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "impeller/compiler/compiler.h" | |
| #include <cstdint> | |
| #include <filesystem> | |
| #include <memory> | |
| #include <optional> | |
| #include <sstream> | |
| #include <string> | |
| #include <utility> | |
| #include "flutter/fml/paths.h" | |
| #include "impeller/base/allocation.h" | |
| #include "impeller/compiler/compiler_backend.h" | |
| #include "impeller/compiler/constants.h" | |
| #include "impeller/compiler/includer.h" | |
| #include "impeller/compiler/logger.h" | |
| #include "impeller/compiler/spirv_compiler.h" | |
| #include "impeller/compiler/types.h" | |
| #include "impeller/compiler/uniform_sorter.h" | |
| #include "impeller/compiler/utilities.h" | |
| namespace impeller { | |
| namespace compiler { | |
| static uint32_t ParseMSLVersion(const std::string& msl_version) { | |
| std::stringstream sstream(msl_version); | |
| std::string version_part; | |
| uint32_t major = 1; | |
| uint32_t minor = 2; | |
| uint32_t patch = 0; | |
| if (std::getline(sstream, version_part, '.')) { | |
| major = std::stoi(version_part); | |
| if (std::getline(sstream, version_part, '.')) { | |
| minor = std::stoi(version_part); | |
| if (std::getline(sstream, version_part, '.')) { | |
| patch = std::stoi(version_part); | |
| } | |
| } | |
| } | |
| if (major < 1 || (major == 1 && minor < 2)) { | |
| std::cerr << "--metal-version version must be at least 1.2. Have " | |
| << msl_version << std::endl; | |
| } | |
| return spirv_cross::CompilerMSL::Options::make_msl_version(major, minor, | |
| patch); | |
| } | |
| static CompilerBackend CreateMSLCompiler( | |
| const spirv_cross::ParsedIR& ir, | |
| const SourceOptions& source_options, | |
| std::optional<uint32_t> msl_version_override = {}) { | |
| auto sl_compiler = std::make_shared<spirv_cross::CompilerMSL>(ir); | |
| spirv_cross::CompilerMSL::Options sl_options; | |
| sl_options.platform = | |
| TargetPlatformToMSLPlatform(source_options.target_platform); | |
| sl_options.msl_version = msl_version_override.value_or( | |
| ParseMSLVersion(source_options.metal_version)); | |
| sl_options.ios_use_simdgroup_functions = | |
| sl_options.is_ios() && | |
| sl_options.msl_version >= | |
| spirv_cross::CompilerMSL::Options::make_msl_version(2, 4, 0); | |
| sl_options.use_framebuffer_fetch_subpasses = true; | |
| sl_compiler->set_msl_options(sl_options); | |
| // Sort the float and sampler uniforms according to their declared/decorated | |
| // order. For user authored fragment shaders, the API for setting uniform | |
| // values uses the index of the uniform in the declared order. By default, the | |
| // metal backend of spirv-cross will order uniforms according to usage. To fix | |
| // this, we use the sorted order and the add_msl_resource_binding API to force | |
| // the ordering to match the declared order. Note that while this code runs | |
| // for all compiled shaders, it will only affect vertex and fragment shaders | |
| // due to the specified stage. | |
| auto floats = | |
| SortUniforms(&ir, sl_compiler.get(), spirv_cross::SPIRType::Float); | |
| auto images = | |
| SortUniforms(&ir, sl_compiler.get(), spirv_cross::SPIRType::SampledImage); | |
| spv::ExecutionModel execution_model = | |
| spv::ExecutionModel::ExecutionModelFragment; | |
| if (source_options.type == SourceType::kVertexShader) { | |
| execution_model = spv::ExecutionModel::ExecutionModelVertex; | |
| } | |
| uint32_t buffer_offset = 0; | |
| uint32_t sampler_offset = 0; | |
| for (auto& float_id : floats) { | |
| sl_compiler->add_msl_resource_binding( | |
| {.stage = execution_model, | |
| .basetype = spirv_cross::SPIRType::BaseType::Float, | |
| .desc_set = sl_compiler->get_decoration(float_id, | |
| spv::DecorationDescriptorSet), | |
| .binding = | |
| sl_compiler->get_decoration(float_id, spv::DecorationBinding), | |
| .count = 1u, | |
| .msl_buffer = buffer_offset}); | |
| buffer_offset++; | |
| } | |
| for (auto& image_id : images) { | |
| sl_compiler->add_msl_resource_binding({ | |
| .stage = execution_model, | |
| .basetype = spirv_cross::SPIRType::BaseType::SampledImage, | |
| .desc_set = | |
| sl_compiler->get_decoration(image_id, spv::DecorationDescriptorSet), | |
| .binding = | |
| sl_compiler->get_decoration(image_id, spv::DecorationBinding), | |
| .count = 1u, | |
| // A sampled image is both an image and a sampler, so both | |
| // offsets need to be set or depending on the partiular shader | |
| // the bindings may be incorrect. | |
| .msl_texture = sampler_offset, | |
| .msl_sampler = sampler_offset, | |
| }); | |
| sampler_offset++; | |
| } | |
| return CompilerBackend(sl_compiler); | |
| } | |
| static CompilerBackend CreateVulkanCompiler( | |
| const spirv_cross::ParsedIR& ir, | |
| const SourceOptions& source_options) { | |
| auto gl_compiler = std::make_shared<spirv_cross::CompilerGLSL>(ir); | |
| spirv_cross::CompilerGLSL::Options sl_options; | |
| sl_options.force_zero_initialized_variables = true; | |
| sl_options.vertex.fixup_clipspace = true; | |
| sl_options.vulkan_semantics = true; | |
| gl_compiler->set_common_options(sl_options); | |
| return CompilerBackend(gl_compiler); | |
| } | |
| static CompilerBackend CreateGLSLCompiler(const spirv_cross::ParsedIR& ir, | |
| const SourceOptions& source_options) { | |
| auto gl_compiler = std::make_shared<spirv_cross::CompilerGLSL>(ir); | |
| // Walk the variables and insert the external image extension if any of them | |
| // begins with the external texture prefix. Unfortunately, we can't walk | |
| // `gl_compiler->get_shader_resources().separate_samplers` until the compiler | |
| // is further along. | |
| // | |
| // Unfortunately, we can't just let the shader author add this extension and | |
| // use `samplerExternalOES` directly because compiling to spirv requires the | |
| // source language profile to be at least 310 ES, but this extension is | |
| // incompatible with ES 310+. | |
| for (auto& id : ir.ids_for_constant_or_variable) { | |
| if (StringStartsWith(ir.get_name(id), kExternalTexturePrefix)) { | |
| gl_compiler->require_extension("GL_OES_EGL_image_external"); | |
| break; | |
| } | |
| } | |
| spirv_cross::CompilerGLSL::Options sl_options; | |
| sl_options.force_zero_initialized_variables = true; | |
| sl_options.vertex.fixup_clipspace = true; | |
| if (source_options.target_platform == TargetPlatform::kOpenGLES || | |
| source_options.target_platform == TargetPlatform::kRuntimeStageGLES) { | |
| sl_options.version = source_options.gles_language_version > 0 | |
| ? source_options.gles_language_version | |
| : 100; | |
| sl_options.es = true; | |
| if (source_options.require_framebuffer_fetch && | |
| source_options.type == SourceType::kFragmentShader) { | |
| gl_compiler->remap_ext_framebuffer_fetch(0, 0, true); | |
| } | |
| gl_compiler->set_variable_type_remap_callback( | |
| [&](const spirv_cross::SPIRType& type, const std::string& var_name, | |
| std::string& name_of_type) { | |
| if (StringStartsWith(var_name, kExternalTexturePrefix)) { | |
| name_of_type = "samplerExternalOES"; | |
| } | |
| }); | |
| } else { | |
| sl_options.version = source_options.gles_language_version > 0 | |
| ? source_options.gles_language_version | |
| : 120; | |
| sl_options.es = false; | |
| } | |
| gl_compiler->set_common_options(sl_options); | |
| return CompilerBackend(gl_compiler); | |
| } | |
| static CompilerBackend CreateSkSLCompiler(const spirv_cross::ParsedIR& ir, | |
| const SourceOptions& source_options) { | |
| auto sksl_compiler = std::make_shared<CompilerSkSL>(ir); | |
| return CompilerBackend(sksl_compiler); | |
| } | |
| static bool EntryPointMustBeNamedMain(TargetPlatform platform) { | |
| switch (platform) { | |
| case TargetPlatform::kUnknown: | |
| FML_UNREACHABLE(); | |
| case TargetPlatform::kMetalDesktop: | |
| case TargetPlatform::kMetalIOS: | |
| case TargetPlatform::kVulkan: | |
| case TargetPlatform::kRuntimeStageMetal: | |
| case TargetPlatform::kRuntimeStageVulkan: | |
| return false; | |
| case TargetPlatform::kSkSL: | |
| case TargetPlatform::kOpenGLES: | |
| case TargetPlatform::kOpenGLDesktop: | |
| case TargetPlatform::kRuntimeStageGLES: | |
| return true; | |
| } | |
| FML_UNREACHABLE(); | |
| } | |
| static CompilerBackend CreateCompiler(const spirv_cross::ParsedIR& ir, | |
| const SourceOptions& source_options) { | |
| CompilerBackend compiler; | |
| switch (source_options.target_platform) { | |
| case TargetPlatform::kMetalDesktop: | |
| case TargetPlatform::kMetalIOS: | |
| case TargetPlatform::kRuntimeStageMetal: | |
| compiler = CreateMSLCompiler(ir, source_options); | |
| break; | |
| case TargetPlatform::kVulkan: | |
| case TargetPlatform::kRuntimeStageVulkan: | |
| compiler = CreateVulkanCompiler(ir, source_options); | |
| break; | |
| case TargetPlatform::kUnknown: | |
| case TargetPlatform::kOpenGLES: | |
| case TargetPlatform::kOpenGLDesktop: | |
| case TargetPlatform::kRuntimeStageGLES: | |
| compiler = CreateGLSLCompiler(ir, source_options); | |
| break; | |
| case TargetPlatform::kSkSL: | |
| compiler = CreateSkSLCompiler(ir, source_options); | |
| } | |
| if (!compiler) { | |
| return {}; | |
| } | |
| auto* backend = compiler.GetCompiler(); | |
| if (!EntryPointMustBeNamedMain(source_options.target_platform) && | |
| source_options.source_language == SourceLanguage::kGLSL) { | |
| backend->rename_entry_point("main", source_options.entry_point_name, | |
| ToExecutionModel(source_options.type)); | |
| } | |
| return compiler; | |
| } | |
| Compiler::Compiler(const std::shared_ptr<const fml::Mapping>& source_mapping, | |
| const SourceOptions& source_options, | |
| Reflector::Options reflector_options) | |
| : options_(source_options) { | |
| if (!source_mapping || source_mapping->GetMapping() == nullptr) { | |
| COMPILER_ERROR(error_stream_) | |
| << "Could not read shader source or shader source was empty."; | |
| return; | |
| } | |
| if (source_options.target_platform == TargetPlatform::kUnknown) { | |
| COMPILER_ERROR(error_stream_) << "Target platform not specified."; | |
| return; | |
| } | |
| SPIRVCompilerOptions spirv_options; | |
| // Make sure reflection is as effective as possible. The generated shaders | |
| // will be processed later by backend specific compilers. | |
| spirv_options.generate_debug_info = true; | |
| switch (options_.source_language) { | |
| case SourceLanguage::kGLSL: | |
| // Expects GLSL 4.60 (Core Profile). | |
| // https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.pdf | |
| spirv_options.source_langauge = | |
| shaderc_source_language::shaderc_source_language_glsl; | |
| spirv_options.source_profile = SPIRVCompilerSourceProfile{ | |
| shaderc_profile::shaderc_profile_core, // | |
| 460, // | |
| }; | |
| break; | |
| case SourceLanguage::kHLSL: | |
| spirv_options.source_langauge = | |
| shaderc_source_language::shaderc_source_language_hlsl; | |
| break; | |
| case SourceLanguage::kUnknown: | |
| COMPILER_ERROR(error_stream_) << "Source language invalid."; | |
| return; | |
| } | |
| switch (source_options.target_platform) { | |
| case TargetPlatform::kMetalDesktop: | |
| case TargetPlatform::kMetalIOS: { | |
| SPIRVCompilerTargetEnv target; | |
| if (source_options.use_half_textures) { | |
| target.env = shaderc_target_env::shaderc_target_env_opengl; | |
| target.version = shaderc_env_version::shaderc_env_version_opengl_4_5; | |
| target.spirv_version = shaderc_spirv_version::shaderc_spirv_version_1_0; | |
| } else { | |
| target.env = shaderc_target_env::shaderc_target_env_vulkan; | |
| target.version = shaderc_env_version::shaderc_env_version_vulkan_1_1; | |
| target.spirv_version = shaderc_spirv_version::shaderc_spirv_version_1_3; | |
| } | |
| spirv_options.target = target; | |
| } break; | |
| case TargetPlatform::kOpenGLES: | |
| case TargetPlatform::kOpenGLDesktop: | |
| case TargetPlatform::kVulkan: | |
| case TargetPlatform::kRuntimeStageVulkan: { | |
| SPIRVCompilerTargetEnv target; | |
| target.env = shaderc_target_env::shaderc_target_env_vulkan; | |
| target.version = shaderc_env_version::shaderc_env_version_vulkan_1_1; | |
| target.spirv_version = shaderc_spirv_version::shaderc_spirv_version_1_3; | |
| if (source_options.target_platform == | |
| TargetPlatform::kRuntimeStageVulkan) { | |
| spirv_options.macro_definitions.push_back("IMPELLER_GRAPHICS_BACKEND"); | |
| spirv_options.relaxed_vulkan_rules = true; | |
| } | |
| spirv_options.target = target; | |
| } break; | |
| case TargetPlatform::kRuntimeStageMetal: | |
| case TargetPlatform::kRuntimeStageGLES: { | |
| SPIRVCompilerTargetEnv target; | |
| target.env = shaderc_target_env::shaderc_target_env_opengl; | |
| target.version = shaderc_env_version::shaderc_env_version_opengl_4_5; | |
| target.spirv_version = shaderc_spirv_version::shaderc_spirv_version_1_0; | |
| spirv_options.target = target; | |
| spirv_options.macro_definitions.push_back("IMPELLER_GRAPHICS_BACKEND"); | |
| } break; | |
| case TargetPlatform::kSkSL: { | |
| SPIRVCompilerTargetEnv target; | |
| target.env = shaderc_target_env::shaderc_target_env_opengl; | |
| target.version = shaderc_env_version::shaderc_env_version_opengl_4_5; | |
| target.spirv_version = shaderc_spirv_version::shaderc_spirv_version_1_0; | |
| // When any optimization level above 'zero' is enabled, the phi merges at | |
| // loop continue blocks are rendered using syntax that is supported in | |
| // GLSL, but not in SkSL. | |
| // https://bugs.chromium.org/p/skia/issues/detail?id=13518. | |
| spirv_options.optimization_level = | |
| shaderc_optimization_level::shaderc_optimization_level_zero; | |
| spirv_options.target = target; | |
| spirv_options.macro_definitions.push_back("SKIA_GRAPHICS_BACKEND"); | |
| } break; | |
| case TargetPlatform::kUnknown: | |
| COMPILER_ERROR(error_stream_) << "Target platform invalid."; | |
| return; | |
| } | |
| // Implicit definition that indicates that this compilation is for the device | |
| // (instead of the host). | |
| spirv_options.macro_definitions.push_back("IMPELLER_DEVICE"); | |
| for (const auto& define : source_options.defines) { | |
| spirv_options.macro_definitions.push_back(define); | |
| } | |
| std::vector<std::string> included_file_names; | |
| spirv_options.includer = std::make_shared<Includer>( | |
| options_.working_directory, options_.include_dirs, | |
| [&included_file_names](auto included_name) { | |
| included_file_names.emplace_back(std::move(included_name)); | |
| }); | |
| // SPIRV Generation. | |
| SPIRVCompiler spv_compiler(source_options, source_mapping); | |
| spirv_assembly_ = spv_compiler.CompileToSPV( | |
| error_stream_, spirv_options.BuildShadercOptions()); | |
| if (!spirv_assembly_) { | |
| return; | |
| } else { | |
| included_file_names_ = std::move(included_file_names); | |
| } | |
| // SL Generation. | |
| spirv_cross::Parser parser( | |
| reinterpret_cast<const uint32_t*>(spirv_assembly_->GetMapping()), | |
| spirv_assembly_->GetSize() / sizeof(uint32_t)); | |
| // The parser and compiler must be run separately because the parser contains | |
| // meta information (like type member names) that are useful for reflection. | |
| parser.parse(); | |
| const auto parsed_ir = | |
| std::make_shared<spirv_cross::ParsedIR>(parser.get_parsed_ir()); | |
| auto sl_compiler = CreateCompiler(*parsed_ir, options_); | |
| if (!sl_compiler) { | |
| COMPILER_ERROR(error_stream_) | |
| << "Could not create compiler for target platform."; | |
| return; | |
| } | |
| // We need to invoke the compiler even if we don't use the SL mapping later | |
| // for Vulkan. The reflector needs information that is only valid after a | |
| // successful compilation call. | |
| auto sl_compilation_result = | |
| CreateMappingWithString(sl_compiler.GetCompiler()->compile()); | |
| // If the target is Vulkan, our shading language is SPIRV which we already | |
| // have. We just need to strip it of debug information. If it isn't, we need | |
| // to invoke the appropriate compiler to compile the SPIRV to the target SL. | |
| if (source_options.target_platform == TargetPlatform::kVulkan || | |
| source_options.target_platform == TargetPlatform::kRuntimeStageVulkan) { | |
| auto stripped_spirv_options = spirv_options; | |
| stripped_spirv_options.generate_debug_info = false; | |
| sl_mapping_ = spv_compiler.CompileToSPV( | |
| error_stream_, stripped_spirv_options.BuildShadercOptions()); | |
| } else { | |
| sl_mapping_ = sl_compilation_result; | |
| } | |
| if (!sl_mapping_) { | |
| COMPILER_ERROR(error_stream_) << "Could not generate SL from SPIRV"; | |
| return; | |
| } | |
| reflector_ = std::make_unique<Reflector>(std::move(reflector_options), // | |
| parsed_ir, // | |
| GetSLShaderSource(), // | |
| sl_compiler // | |
| ); | |
| if (!reflector_->IsValid()) { | |
| COMPILER_ERROR(error_stream_) | |
| << "Could not complete reflection on generated shader."; | |
| return; | |
| } | |
| is_valid_ = true; | |
| } | |
| Compiler::~Compiler() = default; | |
| std::shared_ptr<fml::Mapping> Compiler::GetSPIRVAssembly() const { | |
| return spirv_assembly_; | |
| } | |
| std::shared_ptr<fml::Mapping> Compiler::GetSLShaderSource() const { | |
| return sl_mapping_; | |
| } | |
| bool Compiler::IsValid() const { | |
| return is_valid_; | |
| } | |
| std::string Compiler::GetSourcePrefix() const { | |
| std::stringstream stream; | |
| stream << options_.file_name << ": "; | |
| return stream.str(); | |
| } | |
| std::string Compiler::GetErrorMessages() const { | |
| return error_stream_.str(); | |
| } | |
| const std::vector<std::string>& Compiler::GetIncludedFileNames() const { | |
| return included_file_names_; | |
| } | |
| static std::string JoinStrings(std::vector<std::string> items, | |
| const std::string& separator) { | |
| std::stringstream stream; | |
| for (size_t i = 0, count = items.size(); i < count; i++) { | |
| const auto is_last = (i == count - 1); | |
| stream << items[i]; | |
| if (!is_last) { | |
| stream << separator; | |
| } | |
| } | |
| return stream.str(); | |
| } | |
| std::string Compiler::GetDependencyNames(const std::string& separator) const { | |
| std::vector<std::string> dependencies = included_file_names_; | |
| dependencies.push_back(options_.file_name); | |
| return JoinStrings(dependencies, separator); | |
| } | |
| std::unique_ptr<fml::Mapping> Compiler::CreateDepfileContents( | |
| std::initializer_list<std::string> targets_names) const { | |
| // https://github.com/ninja-build/ninja/blob/master/src/depfile_parser.cc#L28 | |
| const auto targets = JoinStrings(targets_names, " "); | |
| const auto dependencies = GetDependencyNames(" "); | |
| std::stringstream stream; | |
| stream << targets << ": " << dependencies << "\n"; | |
| auto contents = std::make_shared<std::string>(stream.str()); | |
| return std::make_unique<fml::NonOwnedMapping>( | |
| reinterpret_cast<const uint8_t*>(contents->data()), contents->size(), | |
| [contents](auto, auto) {}); | |
| } | |
| const Reflector* Compiler::GetReflector() const { | |
| return reflector_.get(); | |
| } | |
| } // namespace compiler | |
| } // namespace impeller |