Flutter Impeller
shader_bundle.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
7 #include <filesystem>
8 #include <sstream>
9 
10 #include "flutter/fml/file.h"
11 #include "flutter/fml/mapping.h"
16 
19 #include "impeller/shader_bundle/shader_bundle_flatbuffers.h"
20 #include "third_party/json/include/nlohmann/json.hpp"
21 
22 namespace impeller {
23 namespace compiler {
24 
25 std::optional<ShaderBundleConfig> ParseShaderBundleConfig(
26  const std::string& bundle_config_json,
27  std::ostream& error_stream) {
28  auto json = nlohmann::json::parse(bundle_config_json, nullptr, false);
29  if (json.is_discarded() || !json.is_object()) {
30  error_stream << "The shader bundle is not a valid JSON object."
31  << std::endl;
32  return std::nullopt;
33  }
34 
35  ShaderBundleConfig bundle;
36  for (auto& [shader_name, shader_value] : json.items()) {
37  if (bundle.find(shader_name) != bundle.end()) {
38  error_stream << "Duplicate shader \"" << shader_name << "\"."
39  << std::endl;
40  return std::nullopt;
41  }
42  if (!shader_value.is_object()) {
43  error_stream << "Invalid shader entry \"" << shader_name
44  << "\": Entry is not a JSON object." << std::endl;
45  return std::nullopt;
46  }
47 
48  ShaderConfig shader;
49 
50  if (!shader_value.contains("file")) {
51  error_stream << "Invalid shader entry \"" << shader_name
52  << "\": Missing required \"file\" field." << std::endl;
53  return std::nullopt;
54  }
55  shader.source_file_name = shader_value["file"];
56 
57  if (!shader_value.contains("type")) {
58  error_stream << "Invalid shader entry \"" << shader_name
59  << "\": Missing required \"type\" field." << std::endl;
60  return std::nullopt;
61  }
62  shader.type = SourceTypeFromString(shader_value["type"]);
63  if (shader.type == SourceType::kUnknown) {
64  error_stream << "Invalid shader entry \"" << shader_name
65  << "\": Shader type " << shader_value["type"]
66  << " is unknown." << std::endl;
67  return std::nullopt;
68  }
69 
70  shader.language = shader_value.contains("language")
71  ? ToSourceLanguage(shader_value["language"])
73  if (shader.language == SourceLanguage::kUnknown) {
74  error_stream << "Invalid shader entry \"" << shader_name
75  << "\": Unknown language type " << shader_value["language"]
76  << "." << std::endl;
77  return std::nullopt;
78  }
79 
80  shader.entry_point = shader_value.contains("entry_point")
81  ? shader_value["entry_point"]
82  : "main";
83 
84  bundle[shader_name] = shader;
85  }
86 
87  return bundle;
88 }
89 
90 std::vector<std::string_view> GetShaderBundleTargetPlatformDefines(
91  TargetPlatform platform) {
92  switch (platform) {
94  return {"IMPELLER_TARGET_METAL", "IMPELLER_TARGET_METAL_IOS"};
96  return {"IMPELLER_TARGET_METAL", "IMPELLER_TARGET_METAL_DESKTOP"};
98  return {"IMPELLER_TARGET_OPENGLES"};
100  return {"IMPELLER_TARGET_OPENGL"};
102  return {"IMPELLER_TARGET_VULKAN"};
109  return {};
110  }
111  return {};
112 }
113 
114 static std::unique_ptr<fb::shaderbundle::BackendShaderT>
116  SourceOptions& options,
117  const std::string& shader_name,
118  const ShaderConfig& shader_config,
119  std::set<std::string>* out_dependencies) {
120  auto result = std::make_unique<fb::shaderbundle::BackendShaderT>();
121 
122  std::shared_ptr<fml::FileMapping> source_file_mapping =
123  fml::FileMapping::CreateReadOnly(shader_config.source_file_name);
124  if (!source_file_mapping) {
125  std::cerr << "Could not open file for bundled shader \"" << shader_name
126  << "\"." << std::endl;
127  return nullptr;
128  }
129 
130  /// Override options.
131  options.target_platform = target_platform;
132  options.file_name = shader_name; // This is just used for error messages.
133  options.type = shader_config.type;
134  options.source_language = shader_config.language;
136  shader_config.source_file_name, options.type, options.source_language,
137  shader_config.entry_point);
138 
139  // Inject the platform-discriminating defines (e.g. IMPELLER_TARGET_METAL) so
140  // bundled shaders can specialize per backend. These are added to a local copy
141  // because `options` is shared across every backend compiled here; pushing
142  // onto it directly would accumulate defines from previously compiled
143  // backends.
144  SourceOptions backend_options = options;
145  backend_options.target_platform = target_platform;
146  for (const auto& define :
147  GetShaderBundleTargetPlatformDefines(target_platform)) {
148  backend_options.defines.emplace_back(define);
149  }
150 
151  Reflector::Options reflector_options;
152  reflector_options.target_platform = target_platform;
153  reflector_options.entry_point_name = options.entry_point_name;
154  reflector_options.shader_name = shader_name;
155 
156  Compiler compiler(source_file_mapping, backend_options, reflector_options);
157  if (!compiler.IsValid()) {
158  std::cerr << "Compilation failed for bundled shader \"" << shader_name
159  << "\"." << std::endl;
160  std::cerr << compiler.GetErrorMessages() << std::endl;
161  return nullptr;
162  }
163 
164  // Record dependencies so the caller can emit a depfile. The shader's
165  // source file plus every transitive `#include` that contributed to
166  // the compilation. The same source is compiled across multiple
167  // target platforms; the std::set dedupes naturally.
168  if (out_dependencies) {
169  out_dependencies->insert(shader_config.source_file_name);
170  for (const auto& included : compiler.GetIncludedFileNames()) {
171  out_dependencies->insert(included);
172  }
173  }
174 
175  auto reflector = compiler.GetReflector();
176  if (reflector == nullptr) {
177  std::cerr << "Could not create reflector for bundled shader \""
178  << shader_name << "\"." << std::endl;
179  return nullptr;
180  }
181 
182  auto bundle_data = reflector->GetShaderBundleData();
183  if (!bundle_data) {
184  std::cerr << "Bundled shader information was nil for \"" << shader_name
185  << "\"." << std::endl;
186  return nullptr;
187  }
188 
189  result = bundle_data->CreateFlatbuffer();
190  if (!result) {
191  std::cerr << "Failed to create flatbuffer for bundled shader \""
192  << shader_name << "\"." << std::endl;
193  return nullptr;
194  }
195 
196  return result;
197 }
198 
199 static std::unique_ptr<fb::shaderbundle::ShaderT> GenerateShaderFB(
200  SourceOptions options,
201  const std::string& shader_name,
202  const ShaderConfig& shader_config,
203  std::set<std::string>* out_dependencies) {
204  auto result = std::make_unique<fb::shaderbundle::ShaderT>();
205  result->name = shader_name;
206  result->metal_ios =
208  shader_config, out_dependencies);
209  if (!result->metal_ios) {
210  return nullptr;
211  }
212  result->metal_desktop =
214  shader_name, shader_config, out_dependencies);
215  if (!result->metal_desktop) {
216  return nullptr;
217  }
218  result->opengl_es =
220  shader_config, out_dependencies);
221  if (!result->opengl_es) {
222  return nullptr;
223  }
224  result->opengl_desktop =
226  shader_name, shader_config, out_dependencies);
227  if (!result->opengl_desktop) {
228  return nullptr;
229  }
230  result->vulkan =
231  GenerateShaderBackendFB(TargetPlatform::kVulkan, options, shader_name,
232  shader_config, out_dependencies);
233  if (!result->vulkan) {
234  return nullptr;
235  }
236  return result;
237 }
238 
239 std::optional<fb::shaderbundle::ShaderBundleT> GenerateShaderBundleFlatbuffer(
240  const std::string& bundle_config_json,
241  const SourceOptions& options,
242  std::set<std::string>* out_dependencies) {
243  // --------------------------------------------------------------------------
244  /// 1. Parse the bundle configuration.
245  ///
246 
247  std::optional<ShaderBundleConfig> bundle_config =
248  ParseShaderBundleConfig(bundle_config_json, std::cerr);
249  if (!bundle_config) {
250  return std::nullopt;
251  }
252 
253  // --------------------------------------------------------------------------
254  /// 2. Build the deserialized shader bundle.
255  ///
256 
257  fb::shaderbundle::ShaderBundleT shader_bundle;
258  shader_bundle.format_version = static_cast<uint32_t>(
259  fb::shaderbundle::ShaderBundleFormatVersion::kVersion);
260 
261  for (const auto& [shader_name, shader_config] : bundle_config.value()) {
262  std::unique_ptr<fb::shaderbundle::ShaderT> shader =
263  GenerateShaderFB(options, shader_name, shader_config, out_dependencies);
264  if (!shader) {
265  return std::nullopt;
266  }
267  shader_bundle.shaders.push_back(std::move(shader));
268  }
269 
270  return shader_bundle;
271 }
272 
273 /// Write a Ninja-style depfile listing every source file (including
274 /// `#include`d headers) that contributed to the shader bundle at
275 /// `target`.
276 ///
277 /// Format mirrors `Compiler::CreateDepfileContents` for single-shader
278 /// compiles: `<target>: <dep1> <dep2> ... <depN>\n`.
279 /// See
280 /// https://github.com/ninja-build/ninja/blob/master/src/depfile_parser.cc#L28
281 static bool OutputBundleDepfile(const Switches& switches,
282  const std::string& target,
283  const std::set<std::string>& dependencies) {
284  std::stringstream stream;
285  stream << target << ":";
286  for (const auto& dep : dependencies) {
287  stream << " " << dep;
288  }
289  stream << "\n";
290  const auto contents = std::make_shared<std::string>(stream.str());
291  const fml::NonOwnedMapping mapping(
292  reinterpret_cast<const uint8_t*>(contents->data()), contents->size(),
293  [contents](auto, auto) {});
294 
295  // Pass the relative path straight through; fml::WriteAtomically
296  // resolves it against switches.working_directory (a directory fd
297  // representing the build system's intended working dir, which may
298  // differ from std::filesystem::current_path()).
299  if (!fml::WriteAtomically(*switches.working_directory,
300  Utf8FromPath(switches.depfile_path).c_str(),
301  mapping)) {
302  std::cerr << "Could not write depfile to " << switches.depfile_path
303  << std::endl;
304  return false;
305  }
306  return true;
307 }
308 
310  // --------------------------------------------------------------------------
311  /// 1. Parse the shader bundle and generate the flatbuffer result.
312  ///
313  /// Collect dependencies along the way so a depfile can be emitted
314  /// after the bundle is written. The same source file is compiled
315  /// across multiple target platforms; the std::set dedupes naturally.
316  ///
317 
318  std::set<std::string> dependencies;
319  const bool want_depfile = !switches.depfile_path.empty();
320  auto shader_bundle = GenerateShaderBundleFlatbuffer(
321  switches.shader_bundle, switches.CreateSourceOptions(),
322  want_depfile ? &dependencies : nullptr);
323  if (!shader_bundle.has_value()) {
324  // Specific error messages are already handled by
325  // GenerateShaderBundleFlatbuffer.
326  return false;
327  }
328 
329  // --------------------------------------------------------------------------
330  /// 2. Serialize the shader bundle and write to disk.
331  ///
332 
333  auto builder = std::make_shared<flatbuffers::FlatBufferBuilder>();
334  builder->Finish(fb::shaderbundle::ShaderBundle::Pack(*builder.get(),
335  &shader_bundle.value()),
336  fb::shaderbundle::ShaderBundleIdentifier());
337  auto mapping = std::make_shared<fml::NonOwnedMapping>(
338  builder->GetBufferPointer(), builder->GetSize(),
339  [builder](auto, auto) {});
340 
341  auto sl_file_name = std::filesystem::absolute(
342  std::filesystem::current_path() / switches.sl_file_name);
343 
344  if (!fml::WriteAtomically(*switches.working_directory, //
345  Utf8FromPath(sl_file_name).c_str(), //
346  *mapping //
347  )) {
348  std::cerr << "Could not write file to " << switches.sl_file_name
349  << std::endl;
350  return false;
351  }
352  // Tools that consume the runtime stage data expect the access mode to
353  // be 0644.
354  if (!SetPermissiveAccess(sl_file_name)) {
355  return false;
356  }
357 
358  // --------------------------------------------------------------------------
359  /// 3. Output a depfile if one was requested.
360  ///
361  /// Lets build systems (notably Dart's `hooks` framework, which
362  /// `flutter_gpu_shaders`' `buildShaderBundleJson` consumer goes
363  /// through) rerun the bundle build when any contributing source file
364  /// or `#include`d header changes.
365 
366  if (want_depfile) {
367  if (!OutputBundleDepfile(switches, Utf8FromPath(sl_file_name),
368  dependencies)) {
369  return false;
370  }
371  }
372 
373  return true;
374 }
375 
376 } // namespace compiler
377 } // namespace impeller
const Reflector * GetReflector() const
Definition: compiler.cc:748
const std::vector< std::string > & GetIncludedFileNames() const
Definition: compiler.cc:709
std::string GetErrorMessages() const
Definition: compiler.cc:701
std::shared_ptr< ShaderBundleData > GetShaderBundleData() const
Definition: reflector.cc:136
std::filesystem::path sl_file_name
Definition: switches.h:31
std::shared_ptr< fml::UniqueFD > working_directory
Definition: switches.h:24
SourceOptions CreateSourceOptions() const
Definition: switches.cc:319
std::filesystem::path depfile_path
Definition: switches.h:38
bool SetPermissiveAccess(const std::filesystem::path &p)
Sets the file access mode of the file at path 'p' to 0644.
Definition: utilities.cc:16
std::vector< std::string_view > GetShaderBundleTargetPlatformDefines(TargetPlatform platform)
The platform-discriminating preprocessor defines injected when compiling a bundled shader for platfor...
std::optional< fb::shaderbundle::ShaderBundleT > GenerateShaderBundleFlatbuffer(const std::string &bundle_config_json, const SourceOptions &options, std::set< std::string > *out_dependencies)
Parses the JSON shader bundle configuration and invokes the compiler multiple times to produce a shad...
static std::unique_ptr< fb::shaderbundle::ShaderT > GenerateShaderFB(SourceOptions options, const std::string &shader_name, const ShaderConfig &shader_config, std::set< std::string > *out_dependencies)
bool GenerateShaderBundle(Switches &switches)
Parses the JSON shader bundle configuration and invokes the compiler multiple times to produce a shad...
std::unordered_map< std::string, ShaderConfig > ShaderBundleConfig
Definition: types.h:97
std::string EntryPointFunctionNameFromSourceName(const std::filesystem::path &file_name, SourceType type, SourceLanguage source_language, const std::string &entry_point_name)
Definition: types.cc:101
SourceType SourceTypeFromString(std::string name)
Definition: types.cc:34
std::optional< ShaderBundleConfig > ParseShaderBundleConfig(const std::string &bundle_config_json, std::ostream &error_stream)
Parse a shader bundle configuration from a given JSON string.
static bool OutputBundleDepfile(const Switches &switches, const std::string &target, const std::set< std::string > &dependencies)
std::string Utf8FromPath(const std::filesystem::path &path)
Converts a native format path to a utf8 string.
Definition: utilities.cc:30
static std::unique_ptr< fb::shaderbundle::BackendShaderT > GenerateShaderBackendFB(TargetPlatform target_platform, SourceOptions &options, const std::string &shader_name, const ShaderConfig &shader_config, std::set< std::string > *out_dependencies)
SourceLanguage ToSourceLanguage(const std::string &source_language)
Definition: types.cc:52
A shader config parsed as part of a ShaderBundleConfig.
Definition: types.h:90
std::string source_file_name
Definition: types.h:91
SourceLanguage language
Definition: types.h:93
std::filesystem::path file_name
std::vector< std::string > defines