/ src / video_core / custom_textures / custom_tex_manager.cpp
custom_tex_manager.cpp
  1  // Copyright 2023 Citra Emulator Project
  2  // Licensed under GPLv2 or any later version
  3  // Refer to the license.txt file included.
  4  
  5  #include <json.hpp>
  6  #include "common/file_util.h"
  7  #include "common/literals.h"
  8  #include "common/memory_detect.h"
  9  #include "common/microprofile.h"
 10  #include "common/settings.h"
 11  #include "common/string_util.h"
 12  #include "common/texture.h"
 13  #include "core/core.h"
 14  #include "core/frontend/image_interface.h"
 15  #include "core/hle/kernel/kernel.h"
 16  #include "core/hle/kernel/process.h"
 17  #include "video_core/custom_textures/custom_tex_manager.h"
 18  #include "video_core/rasterizer_cache/surface_params.h"
 19  #include "video_core/rasterizer_cache/utils.h"
 20  
 21  namespace VideoCore {
 22  
 23  namespace {
 24  
 25  MICROPROFILE_DEFINE(CustomTexManager_TickFrame, "CustomTexManager", "TickFrame",
 26                      MP_RGB(54, 16, 32));
 27  
 28  constexpr std::size_t MAX_UPLOADS_PER_TICK = 8;
 29  
 30  using namespace Common::Literals;
 31  
 32  bool IsPow2(u32 value) {
 33      return value != 0 && (value & (value - 1)) == 0;
 34  }
 35  
 36  CustomFileFormat MakeFileFormat(std::string_view ext) {
 37      if (ext == "png") {
 38          return CustomFileFormat::PNG;
 39      } else if (ext == "dds") {
 40          return CustomFileFormat::DDS;
 41      } else if (ext == "ktx") {
 42          return CustomFileFormat::KTX;
 43      }
 44      return CustomFileFormat::None;
 45  }
 46  
 47  MapType MakeMapType(std::string_view ext) {
 48      if (ext == "norm") {
 49          return MapType::Normal;
 50      }
 51      LOG_ERROR(Render, "Unknown material extension {}", ext);
 52      return MapType::Color;
 53  }
 54  
 55  } // Anonymous namespace
 56  
 57  CustomTexManager::CustomTexManager(Core::System& system_)
 58      : system{system_}, image_interface{*system.GetImageInterface()},
 59        async_custom_loading{Settings::values.async_custom_loading.GetValue()} {}
 60  
 61  CustomTexManager::~CustomTexManager() = default;
 62  
 63  void CustomTexManager::TickFrame() {
 64      MICROPROFILE_SCOPE(CustomTexManager_TickFrame);
 65      if (!textures_loaded) {
 66          return;
 67      }
 68      std::size_t num_uploads = 0;
 69      for (auto it = async_uploads.begin(); it != async_uploads.end();) {
 70          if (num_uploads >= MAX_UPLOADS_PER_TICK) {
 71              return;
 72          }
 73          switch (it->material->state) {
 74          case DecodeState::Decoded:
 75              it->func();
 76              num_uploads++;
 77              [[fallthrough]];
 78          case DecodeState::Failed:
 79              it = async_uploads.erase(it);
 80              continue;
 81          default:
 82              it++;
 83              break;
 84          }
 85      }
 86  }
 87  
 88  void CustomTexManager::FindCustomTextures() {
 89      if (textures_loaded) {
 90          return;
 91      }
 92      if (!workers) {
 93          CreateWorkers();
 94      }
 95  
 96      const u64 title_id = system.Kernel().GetCurrentProcess()->codeset->program_id;
 97      const auto textures = GetTextures(title_id);
 98      if (!ReadConfig(title_id)) {
 99          use_new_hash = false;
100          skip_mipmap = true;
101      }
102  
103      custom_textures.reserve(textures.size());
104      for (const FileUtil::FSTEntry& file : textures) {
105          if (file.isDirectory) {
106              continue;
107          }
108          custom_textures.push_back(std::make_unique<CustomTexture>(image_interface));
109          CustomTexture* const texture{custom_textures.back().get()};
110          if (!ParseFilename(file, texture)) {
111              continue;
112          }
113          for (const u64 hash : texture->hashes) {
114              auto& material = material_map[hash];
115              if (!material) {
116                  material = std::make_unique<Material>();
117              }
118              material->hash = hash;
119              material->AddMapTexture(texture);
120          }
121      }
122      textures_loaded = true;
123  }
124  
125  bool CustomTexManager::ParseFilename(const FileUtil::FSTEntry& file, CustomTexture* texture) {
126      auto parts = Common::SplitString(file.virtualName, '.');
127      if (parts.size() > 3) {
128          LOG_ERROR(Render, "Invalid filename {}, ignoring", file.virtualName);
129          return false;
130      }
131      // The last string should always be the file extension.
132      const CustomFileFormat file_format = MakeFileFormat(parts.back());
133      if (file_format == CustomFileFormat::None) {
134          return false;
135      }
136      if (file_format == CustomFileFormat::DDS && skip_mipmap) {
137          LOG_ERROR(Render, "Mipmap skip is incompatible with DDS textures, skipping!");
138          return false;
139      }
140      texture->file_format = file_format;
141      parts.pop_back();
142  
143      // This means the texture is a material type other than color.
144      texture->type = MapType::Color;
145      if (parts.size() > 1) {
146          texture->type = MakeMapType(parts.back());
147          parts.pop_back();
148      }
149  
150      // First look if this file is mapped to any number of hashes.
151      std::vector<u64>& hashes = texture->hashes;
152      const auto it = path_to_hash_map.find(file.virtualName);
153      if (it != path_to_hash_map.end()) {
154          hashes = it->second;
155      }
156  
157      // It's also possible for pack creators to retain the default texture name
158      // still map the texture to another hash. Support that as well.
159      u32 width;
160      u32 height;
161      u32 format;
162      unsigned long long hash{};
163      const bool is_parsed = std::sscanf(parts.back().c_str(), "tex1_%ux%u_%llX_%u", &width, &height,
164                                         &hash, &format) == 4;
165      const bool is_mapped =
166          !hashes.empty() && std::find(hashes.begin(), hashes.end(), hash) != hashes.end();
167      if (is_parsed && !is_mapped) {
168          hashes.push_back(hash);
169      }
170  
171      texture->path = file.physicalName;
172      return true;
173  }
174  
175  void CustomTexManager::PrepareDumping(u64 title_id) {
176      // If a pack exists in the load folder that uses the old hash, dump textures using the old hash.
177      // This occurs either if a configuration file doesn't exist or that file sets the old hash.
178      const std::string load_path =
179          fmt::format("{}textures/{:016X}/", GetUserPath(FileUtil::UserPath::LoadDir), title_id);
180      if (FileUtil::Exists(load_path) && !ReadConfig(title_id, true)) {
181          use_new_hash = false;
182      }
183  
184      // Write template config file
185      const std::string dump_path =
186          fmt::format("{}textures/{:016X}/", GetUserPath(FileUtil::UserPath::DumpDir), title_id);
187      const std::string pack_config = dump_path + "pack.json";
188      if (FileUtil::Exists(pack_config)) {
189          return;
190      }
191  
192      nlohmann::ordered_json json;
193      json["author"] = "citra";
194      json["version"] = "1.0.0";
195      json["description"] = "A graphics pack";
196  
197      auto& options = json["options"];
198      options["skip_mipmap"] = false;
199      options["flip_png_files"] = true;
200      options["use_new_hash"] = true;
201  
202      FileUtil::IOFile file{pack_config, "w"};
203      const std::string output = json.dump(4);
204      file.WriteString(output);
205  }
206  
207  void CustomTexManager::PreloadTextures(const std::atomic_bool& stop_run,
208                                         const VideoCore::DiskResourceLoadCallback& callback) {
209      u64 size_sum = 0;
210      std::size_t preloaded = 0;
211      const u64 sys_mem = Common::GetMemInfo().total_physical_memory;
212      const u64 recommended_min_mem = 2_GiB;
213  
214      // keep 2GiB memory for system stability if system RAM is 4GiB+ - use half of memory in other
215      // cases
216      const u64 max_mem =
217          (sys_mem / 2 < recommended_min_mem) ? (sys_mem / 2) : (sys_mem - recommended_min_mem);
218  
219      workers->QueueWork([&]() {
220          for (auto& [hash, material] : material_map) {
221              if (size_sum > max_mem) {
222                  LOG_WARNING(Render, "Aborting texture preload due to insufficient memory");
223                  return;
224              }
225              if (stop_run) {
226                  return;
227              }
228              material->LoadFromDisk(flip_png_files);
229              size_sum += material->size;
230              if (callback) {
231                  callback(VideoCore::LoadCallbackStage::Preload, preloaded, custom_textures.size());
232              }
233              preloaded++;
234          }
235      });
236      workers->WaitForRequests();
237      async_custom_loading = false;
238  }
239  
240  void CustomTexManager::DumpTexture(const SurfaceParams& params, u32 level, std::span<u8> data,
241                                     u64 data_hash) {
242      const u64 program_id = system.Kernel().GetCurrentProcess()->codeset->program_id;
243      const u32 data_size = static_cast<u32>(data.size());
244      const u32 width = params.width;
245      const u32 height = params.height;
246      const PixelFormat format = params.pixel_format;
247  
248      std::string dump_path = fmt::format(
249          "{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id);
250      if (!FileUtil::CreateFullPath(dump_path)) {
251          LOG_ERROR(Render, "Unable to create {}", dump_path);
252          return;
253      }
254  
255      dump_path +=
256          fmt::format("tex1_{}x{}_{:016X}_{}_mip{}.png", width, height, data_hash, format, level);
257      if (dumped_textures.contains(data_hash) || FileUtil::Exists(dump_path)) {
258          return;
259      }
260  
261      // Make sure the texture size is a power of 2.
262      // If not, the surface is probably a framebuffer
263      if (!IsPow2(width) || !IsPow2(height)) {
264          LOG_WARNING(Render, "Not dumping {:016X} because size isn't a power of 2 ({}x{})",
265                      data_hash, width, height);
266          return;
267      }
268  
269      const u32 decoded_size = width * height * 4;
270      std::vector<u8> pixels(data_size + decoded_size);
271      std::memcpy(pixels.data(), data.data(), data_size);
272  
273      auto dump = [this, width, height, params, data_size, decoded_size, pixels = std::move(pixels),
274                   dump_path = std::move(dump_path)]() mutable {
275          const std::span encoded = std::span{pixels}.first(data_size);
276          const std::span decoded = std::span{pixels}.last(decoded_size);
277          DecodeTexture(params, params.addr, params.end, encoded, decoded,
278                        params.type == SurfaceType::Color);
279          Common::FlipRGBA8Texture(decoded, width, height);
280          image_interface.EncodePNG(dump_path, width, height, decoded);
281      };
282      if (!workers) {
283          CreateWorkers();
284      }
285      workers->QueueWork(std::move(dump));
286      dumped_textures.insert(data_hash);
287  }
288  
289  Material* CustomTexManager::GetMaterial(u64 data_hash) {
290      const auto it = material_map.find(data_hash);
291      if (it == material_map.end()) {
292          LOG_WARNING(Render, "Unable to find replacement for surface with hash {:016X}", data_hash);
293          return nullptr;
294      }
295      return it->second.get();
296  }
297  
298  bool CustomTexManager::Decode(Material* material, std::function<bool()>&& upload) {
299      if (!async_custom_loading) {
300          material->LoadFromDisk(flip_png_files);
301          return upload();
302      }
303      if (material->IsUnloaded()) {
304          material->state = DecodeState::Pending;
305          workers->QueueWork([material, this] { material->LoadFromDisk(flip_png_files); });
306      }
307      async_uploads.push_back({
308          .material = material,
309          .func = std::move(upload),
310      });
311      return false;
312  }
313  
314  bool CustomTexManager::ReadConfig(u64 title_id, bool options_only) {
315      const std::string load_path =
316          fmt::format("{}textures/{:016X}/", GetUserPath(FileUtil::UserPath::LoadDir), title_id);
317      if (!FileUtil::Exists(load_path)) {
318          FileUtil::CreateFullPath(load_path);
319      }
320  
321      const std::string config_path = load_path + "pack.json";
322      FileUtil::IOFile config_file{config_path, "r"};
323      if (!config_file.IsOpen()) {
324          LOG_INFO(Render, "Unable to find pack config file, using legacy defaults");
325          return false;
326      }
327      std::string config(config_file.GetSize(), '\0');
328      const std::size_t read_size = config_file.ReadBytes(config.data(), config.size());
329      if (!read_size) {
330          return false;
331      }
332  
333      nlohmann::json json = nlohmann::json::parse(config, nullptr, false, true);
334  
335      const auto& options = json["options"];
336      skip_mipmap = options["skip_mipmap"].get<bool>();
337      flip_png_files = options["flip_png_files"].get<bool>();
338      use_new_hash = options["use_new_hash"].get<bool>();
339  
340      if (options_only) {
341          return true;
342      }
343  
344      const auto& textures = json["textures"];
345      for (const auto& material : textures.items()) {
346          std::size_t idx{};
347          const u64 hash = std::stoull(material.key(), &idx, 16);
348          if (!idx) {
349              LOG_ERROR(Render, "Key {} is invalid, skipping", material.key());
350              continue;
351          }
352          const auto parse = [&](const std::string& file) {
353              const std::string filename{FileUtil::GetFilename(file)};
354              auto [it, new_hash] = path_to_hash_map.try_emplace(filename);
355              it->second.push_back(hash);
356          };
357          const auto value = material.value();
358          if (value.is_string()) {
359              const auto file = value.get<std::string>();
360              parse(file);
361          } else if (value.is_array()) {
362              const auto files = value.get<std::vector<std::string>>();
363              for (const std::string& file : files) {
364                  parse(file);
365              }
366          } else {
367              LOG_ERROR(Render, "Material with key {} is invalid", material.key());
368          }
369      }
370      return true;
371  }
372  
373  std::vector<FileUtil::FSTEntry> CustomTexManager::GetTextures(u64 title_id) {
374      const std::string load_path =
375          fmt::format("{}textures/{:016X}/", GetUserPath(FileUtil::UserPath::LoadDir), title_id);
376      if (!FileUtil::Exists(load_path)) {
377          FileUtil::CreateFullPath(load_path);
378      }
379  
380      FileUtil::FSTEntry texture_dir;
381      std::vector<FileUtil::FSTEntry> textures;
382      FileUtil::ScanDirectoryTree(load_path, texture_dir, 64);
383      FileUtil::GetAllFilesFromNestedEntries(texture_dir, textures);
384      return textures;
385  }
386  
387  void CustomTexManager::CreateWorkers() {
388      const std::size_t num_workers = std::max(std::thread::hardware_concurrency(), 2U) - 1;
389      workers = std::make_unique<Common::ThreadWorker>(num_workers, "Custom textures");
390  }
391  
392  } // namespace VideoCore