| // 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 <string_view> | |
| #include <utility> | |
| #include "flutter/fml/paths.h" | |
| #include "flutter/third_party/re2/re2/re2.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" | |
| #include "third_party/skia/include/core/SkString.h" | |
| #include "third_party/skia/include/effects/SkRuntimeEffect.h" | |
| namespace impeller { | |
| namespace compiler { | |
| namespace { | |
| constexpr const char* kEGLImageExternalExtension = "GL_OES_EGL_image_external"; | |
| constexpr const char* kEGLImageExternalExtension300 = | |
| "GL_OES_EGL_image_external_essl3"; | |
| constexpr int kVerboseErrorLineThreshold = 6; | |
| // Set per pass by RenderPassGLES: +1.0 swapchain, -1.0 offscreen FBO. See | |
| // flutter/flutter#186554. | |
| constexpr const char* kYFlipUniformName = "_impeller_y_flip"; | |
| constexpr const char* kUserMainName = "_impeller_user_main"; | |
| bool IsGLTargetPlatform(TargetPlatform platform) { | |
| return platform == TargetPlatform::kOpenGLES || | |
| platform == TargetPlatform::kOpenGLDesktop || | |
| platform == TargetPlatform::kRuntimeStageGLES || | |
| platform == TargetPlatform::kRuntimeStageGLES3; | |
| } | |
| // Wraps the SPIRV-Cross-emitted entry point so `gl_Position.y *= | |
| // _impeller_y_flip;` runs on every exit, including early returns. Renames | |
| // `void main(` to `void _impeller_user_main(` and appends a wrapper that | |
| // calls it then applies the flip. | |
| std::string InjectYFlipForGLESVertexShader(std::string source) { | |
| // Anchor on leading newline; spirv-cross emits the entry point at file | |
| // scope, so the match is unambiguous in its comment-free output. | |
| constexpr std::string_view kMainPattern = "\nvoid main("; | |
| const size_t main_pos = source.find(kMainPattern); | |
| if (main_pos == std::string::npos) { | |
| return source; | |
| } | |
| const std::string user_main_decl = | |
| std::string("\nvoid ") + kUserMainName + "("; | |
| source.replace(main_pos, kMainPattern.size(), user_main_decl); | |
| std::string wrapper = "\nvoid main() {\n "; | |
| wrapper += kUserMainName; | |
| wrapper += "();\n gl_Position.y *= "; | |
| wrapper += kYFlipUniformName; | |
| wrapper += ";\n}\n"; | |
| source.append(wrapper); | |
| // Declare the uniform after the last `precision` directive, falling | |
| // back to right after `#version`, falling back to the top. | |
| const std::string declaration = | |
| std::string("\nuniform float ") + kYFlipUniformName + ";\n"; | |
| size_t inject_at = std::string::npos; | |
| for (size_t pos = source.find("\nprecision "); pos != std::string::npos; | |
| pos = source.find("\nprecision ", pos + 1)) { | |
| const size_t eol = source.find('\n', pos + 1); | |
| if (eol == std::string::npos) { | |
| break; | |
| } | |
| inject_at = eol; | |
| } | |
| if (inject_at == std::string::npos) { | |
| const size_t version_pos = source.find("#version"); | |
| if (version_pos != std::string::npos) { | |
| inject_at = source.find('\n', version_pos); | |
| } | |
| } | |
| if (inject_at == std::string::npos) { | |
| inject_at = 0; | |
| } | |
| source.insert(inject_at, declaration); | |
| return source; | |
| } | |
| } // namespace | |
| // This value should be <= 7372. UBOs can be larger on some devices but a | |
| // performance cost will be paid. | |
| // https://docs.qualcomm.com/bundle/publicresource/topics/80-78185-2/best_practices.html?product=1601111740035277#buffer-best-practices | |
| static const uint32_t kMaxUniformBufferSize = 6208; | |
| 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)) { | |
| if (source_options.gles_language_version >= 300) { | |
| gl_compiler->require_extension(kEGLImageExternalExtension300); | |
| } else { | |
| gl_compiler->require_extension(kEGLImageExternalExtension); | |
| } | |
| 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 || | |
| source_options.target_platform == TargetPlatform::kRuntimeStageGLES3) { | |
| sl_options.version = source_options.gles_language_version > 0 | |
| ? source_options.gles_language_version | |
| : 100; | |
| // If we have requested GLES3 and/or Compute Shaders, | |
| // promote the language version accordingly | |
| if (source_options.type == SourceType::kComputeShader && | |
| sl_options.version < 310) { | |
| sl_options.version = 310; | |
| } else if (source_options.target_platform == | |
| TargetPlatform::kRuntimeStageGLES3 && | |
| sl_options.version < 300) { | |
| sl_options.version = 300; | |
| } | |
| 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; | |
| if (source_options.type == SourceType::kComputeShader && | |
| sl_options.version < 430) { | |
| sl_options.version = 430; | |
| } | |
| 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: | |
| case TargetPlatform::kRuntimeStageGLES3: | |
| 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::kOpenGLES: | |
| case TargetPlatform::kOpenGLDesktop: | |
| case TargetPlatform::kRuntimeStageGLES: | |
| case TargetPlatform::kRuntimeStageGLES3: | |
| compiler = CreateGLSLCompiler(ir, source_options); | |
| break; | |
| case TargetPlatform::kSkSL: | |
| compiler = CreateSkSLCompiler(ir, source_options); | |
| break; | |
| case TargetPlatform::kUnknown: | |
| FML_UNREACHABLE(); | |
| } | |
| 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; | |
| } | |
| namespace { | |
| uint32_t CalculateUBOSize(const spirv_cross::Compiler* compiler) { | |
| spirv_cross::ShaderResources resources = compiler->get_shader_resources(); | |
| uint32_t result = 0; | |
| for (const spirv_cross::Resource& ubo : resources.uniform_buffers) { | |
| const spirv_cross::SPIRType& ubo_type = | |
| compiler->get_type(ubo.base_type_id); | |
| uint32_t size = compiler->get_declared_struct_size(ubo_type); | |
| result += size; | |
| } | |
| return result; | |
| } | |
| // Mirrors BufferBindingsGLES's uniform key normalization: ASCII uppercase | |
| // and drop underscores. GLSL identifiers are ASCII, so this is locale-safe. | |
| std::string StripUnderscoresAndUpper(std::string_view name) { | |
| std::string result; | |
| result.reserve(name.size()); | |
| for (char ch : name) { | |
| if (ch == '_') { | |
| continue; | |
| } | |
| if (ch >= 'a' && ch <= 'z') { | |
| result.push_back(static_cast<char>(ch - 'a' + 'A')); | |
| } else { | |
| result.push_back(ch); | |
| } | |
| } | |
| return result; | |
| } | |
| // On pre-`#version 140` GL targets, SPIRV-Cross lowers each uniform block to a | |
| // flat `uniform StructType instanceName;` and BufferBindingsGLES resolves | |
| // members by the block name modulo `StripUnderscoresAndUpper`. A | |
| // non-conforming instance name (e.g. `uniform ToonInfo { ... } toon;`) causes | |
| // every member to silently bind to GL location `-1`. Rename the instance to | |
| // `_<BlockName>`, which reduces to the block name under the same fold. | |
| // See flutter/flutter#186393. | |
| void CanonicalizeUniformBlockInstanceNamesForGL( | |
| TargetPlatform target_platform, | |
| spirv_cross::Compiler* compiler) { | |
| if (target_platform != TargetPlatform::kOpenGLES && | |
| target_platform != TargetPlatform::kOpenGLDesktop) { | |
| return; | |
| } | |
| for (const spirv_cross::Resource& ubo : | |
| compiler->get_shader_resources().uniform_buffers) { | |
| const std::string block_name = compiler->get_name(ubo.base_type_id); | |
| const std::string instance_name = compiler->get_name(ubo.id); | |
| if (StripUnderscoresAndUpper(instance_name) == | |
| StripUnderscoresAndUpper(block_name)) { | |
| continue; | |
| } | |
| compiler->set_name(ubo.id, "_" + block_name); | |
| } | |
| } | |
| /// Strips C-style single-line (//...) and multi-line (/*...*/) comments from | |
| /// the GLSL shader source. | |
| static std::string StripComments(std::string_view source) { | |
| std::string result(source); | |
| static const re2::LazyRE2 kCommentPattern = {R"(//[^\n]*|/\*[\s\S]*?\*/)"}; | |
| re2::RE2::GlobalReplace(&result, *kCommentPattern, ""); | |
| return result; | |
| } | |
| /// Checks if the fragment shader source contains multiple return statements or | |
| /// an early return within the `main()` function. | |
| static bool CheckHasMultipleReturnsInMain(std::string_view source) { | |
| std::string clean_source = StripComments(source); | |
| // Match the declaration of main(...) { | |
| static const re2::LazyRE2 kMainPattern = {R"((\bmain\s*\([^)]*\)\s*\{))"}; | |
| re2::StringPiece match; | |
| if (!re2::RE2::PartialMatch(clean_source, *kMainPattern, &match)) { | |
| return false; | |
| } | |
| size_t main_offset = match.data() - clean_source.data(); | |
| size_t open_brace = clean_source.find('{', main_offset); | |
| if (open_brace == std::string::npos) { | |
| return false; | |
| } | |
| // Find the matching closing brace for main(). | |
| int brace_depth = 1; | |
| size_t pos = open_brace + 1; | |
| while (pos < clean_source.size() && brace_depth > 0) { | |
| char c = clean_source[pos]; | |
| if (c == '{') { | |
| brace_depth++; | |
| } else if (c == '}') { | |
| brace_depth--; | |
| } | |
| pos++; | |
| } | |
| size_t main_body_len = | |
| (pos > open_brace + 1) ? (pos - 1) - (open_brace + 1) : 0; | |
| re2::StringPiece main_body(clean_source.data() + open_brace + 1, | |
| main_body_len); | |
| static const re2::LazyRE2 kReturnPattern = {R"(\breturn\b)"}; | |
| return re2::RE2::PartialMatch(main_body, *kReturnPattern); | |
| } | |
| static bool CheckHasSamplers(const CompilerBackend& sl_compiler) { | |
| if (!sl_compiler.GetCompiler()) { | |
| return false; | |
| } | |
| auto resources = sl_compiler.GetCompiler()->get_shader_resources(); | |
| return !resources.sampled_images.empty() || | |
| !resources.separate_samplers.empty() || | |
| !resources.separate_images.empty(); | |
| } | |
| } // namespace | |
| 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: | |
| case TargetPlatform::kRuntimeStageGLES3: { | |
| 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"); | |
| if (source_options.target_platform == TargetPlatform::kRuntimeStageGLES || | |
| source_options.target_platform == | |
| TargetPlatform::kRuntimeStageGLES3) { | |
| spirv_options.macro_definitions.push_back("IMPELLER_TARGET_OPENGLES"); | |
| // A temporary macro that allows fragment shader authors to target | |
| // Flutter <= 3.44 before the OpenGLES flip was removed. | |
| spirv_options.macro_definitions.push_back( | |
| "IMPELLER_OPENGLES_UNFLIPPED_DEPRECATED"); | |
| } | |
| } 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; | |
| } | |
| CanonicalizeUniformBlockInstanceNamesForGL(source_options.target_platform, | |
| sl_compiler.GetCompiler()); | |
| uint32_t ubo_size = CalculateUBOSize(sl_compiler.GetCompiler()); | |
| if (ubo_size > kMaxUniformBufferSize) { | |
| COMPILER_ERROR(error_stream_) << "Uniform buffer size exceeds max (" | |
| << kMaxUniformBufferSize << "): " << ubo_size; | |
| 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_str = sl_compiler.GetCompiler()->compile(); | |
| // GL vertex shaders get a y-flip epilogue; see | |
| // https://github.com/flutter/flutter/issues/186554. | |
| if (IsGLTargetPlatform(source_options.target_platform) && | |
| source_options.type == SourceType::kVertexShader) { | |
| sl_compilation_result_str = | |
| InjectYFlipForGLESVertexShader(std::move(sl_compilation_result_str)); | |
| } | |
| auto sl_compilation_result = | |
| CreateMappingWithString(sl_compilation_result_str); | |
| // 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; | |
| } | |
| if (sl_compiler.GetType() == CompilerBackend::Type::kSkSL && | |
| !ValidateSkSLResult(sl_compilation_result_str).ok()) { | |
| 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; | |
| } | |
| std::string source_str( | |
| reinterpret_cast<const char*>(source_mapping->GetMapping()), | |
| source_mapping->GetSize()); | |
| if (source_options.type == SourceType::kFragmentShader && | |
| CheckHasSamplers(sl_compiler) && | |
| CheckHasMultipleReturnsInMain(source_str)) { | |
| COMPILER_WARNING(warning_stream_) | |
| << "Fragment shader uses texture samplers alongside multiple 'return' " | |
| "statements in `main()`. " | |
| << "This has the potential to cause compilation errors during shader " | |
| "compilation on Windows. \n" | |
| << "To avoid crashes on Windows, consider removing multiple 'return;' " | |
| "statements in `main()`." | |
| << "See https://github.com/flutter/flutter/issues/190809 for details."; | |
| } | |
| is_valid_ = true; | |
| } | |
| absl::Status Compiler::ValidateSkSLResult(const std::string& sksl) { | |
| // Validate compiled SkSL by trying to create a SkRuntimeEffect. | |
| SkRuntimeEffect::Result result = | |
| SkRuntimeEffect::MakeForShader(SkString(sksl)); | |
| if (result.effect != nullptr) { | |
| return absl::OkStatus(); | |
| } | |
| // SkSL is invalid. Output the SkSL and the error. | |
| std::stringstream output; | |
| bool is_truncated = false; | |
| // Lambda to append text to the output stream, truncating if needed. | |
| auto append_and_truncate = [&](const std::string& text) { | |
| std::stringstream text_stream(text); | |
| std::string line; | |
| int lines_outputted = 0; | |
| while (std::getline(text_stream, line)) { | |
| output << "\n " << line; | |
| if (++lines_outputted == kVerboseErrorLineThreshold) { | |
| break; | |
| } | |
| } | |
| if (lines_outputted == kVerboseErrorLineThreshold) { | |
| auto full_line_count = std::count(text.begin(), text.end(), '\n') + 1; | |
| output << "\n... (truncated " << full_line_count - lines_outputted | |
| << " lines)"; | |
| is_truncated = true; | |
| } | |
| }; | |
| output << "\nCompiled to invalid SkSL:"; | |
| append_and_truncate(sksl); | |
| output << "\nSkSL Error:"; | |
| std::string error_text(result.errorText.c_str()); | |
| append_and_truncate(error_text); | |
| // Output maybe-truncated SkSL and error to error_stream_. | |
| COMPILER_ERROR(error_stream_) << output.str(); | |
| // If the output was truncated, output the full SkSL and error to | |
| // verbose_error_stream_. | |
| if (is_truncated) { | |
| COMPILER_ERROR(verbose_error_stream_) << "\nCompiled to invalid SkSL:\n" | |
| << sksl << "\nSkSL Error:\n" | |
| << error_text; | |
| } | |
| return absl::InternalError("SkSL validation failed."); | |
| } | |
| 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(); | |
| } | |
| std::string Compiler::GetWarningMessages() const { | |
| return warning_stream_.str(); | |
| } | |
| std::string Compiler::GetVerboseErrorMessages() const { | |
| return verbose_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(Utf8FromPath(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 |