// Copyright 2023 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/Assets/DirectFilesystemAssetLibrary.h" #include #include #include #include "Common/FileUtil.h" #include "Common/IOFile.h" #include "Common/Logging/Log.h" #include "Common/StringUtil.h" #include "VideoCommon/Assets/MaterialAsset.h" #include "VideoCommon/Assets/MeshAsset.h" #include "VideoCommon/Assets/ShaderAsset.h" #include "VideoCommon/Assets/TextureAsset.h" #include "VideoCommon/RenderState.h" namespace VideoCommon { namespace { std::chrono::system_clock::time_point FileTimeToSysTime(std::filesystem::file_time_type file_time) { #ifdef _WIN32 return std::chrono::clock_cast(file_time); #else // Note: all compilers should switch to chrono::clock_cast // once it is available for use const auto system_time_now = std::chrono::system_clock::now(); const auto file_time_now = decltype(file_time)::clock::now(); return std::chrono::time_point_cast( file_time - file_time_now + system_time_now); #endif } std::size_t GetAssetSize(const CustomTextureData& data) { std::size_t total = 0; for (const auto& slice : data.m_slices) { for (const auto& level : slice.m_levels) { total += level.data.size(); } } return total; } } // namespace CustomAssetLibrary::TimeType DirectFilesystemAssetLibrary::GetLastAssetWriteTime(const AssetID& asset_id) const { std::lock_guard lk(m_lock); if (auto iter = m_assetid_to_asset_map_path.find(asset_id); iter != m_assetid_to_asset_map_path.end()) { const auto& asset_map_path = iter->second; CustomAssetLibrary::TimeType max_entry; for (const auto& [key, value] : asset_map_path) { std::error_code ec; const auto tp = std::filesystem::last_write_time(value, ec); if (ec) continue; auto tp_sys = FileTimeToSysTime(tp); if (tp_sys > max_entry) max_entry = tp_sys; } return max_entry; } return {}; } CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) { const auto asset_map = GetAssetMapForID(asset_id); // Asset map for a pixel shader is the shader and some metadata if (asset_map.size() != 2) { ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have two files mapped!", asset_id); return {}; } const auto metadata = asset_map.find("metadata"); const auto shader = asset_map.find("shader"); if (metadata == asset_map.end()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a metadata entry mapped!", asset_id); return {}; } if (shader == asset_map.end()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a shader entry mapped!", asset_id); return {}; } std::size_t metadata_size; { std::error_code ec; metadata_size = std::filesystem::file_size(metadata->second, ec); if (ec) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to get shader metadata file size with error '{}'!", asset_id, ec); return {}; } } std::size_t shader_size; { std::error_code ec; shader_size = std::filesystem::file_size(shader->second, ec); if (ec) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to get shader source file size with error '{}'!", asset_id, ec); return {}; } } const auto approx_mem_size = metadata_size + shader_size; if (!File::ReadFileToString(PathToString(shader->second), data->m_shader_source)) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the shader file '{}',", asset_id, PathToString(shader->second)); return {}; } std::string json_data; if (!File::ReadFileToString(PathToString(metadata->second), json_data)) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the json file '{}',", asset_id, PathToString(metadata->second)); return {}; } picojson::value root; const auto error = picojson::parse(root, json_data); if (!error.empty()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the json file '{}', due to parse error: {}", asset_id, PathToString(metadata->second), error); return {}; } if (!root.is()) { ERROR_LOG_FMT( VIDEO, "Asset '{}' error - failed to load the json file '{}', due to root not being an object!", asset_id, PathToString(metadata->second)); return {}; } const auto& root_obj = root.get(); if (!PixelShaderData::FromJson(asset_id, root_obj, data)) return {}; return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)}; } CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMaterial(const AssetID& asset_id, MaterialData* data) { const auto asset_map = GetAssetMapForID(asset_id); // Material is expected to have one asset mapped if (asset_map.empty() || asset_map.size() > 1) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - material expected to have one file mapped!", asset_id); return {}; } const auto& asset_path = asset_map.begin()->second; std::string json_data; if (!File::ReadFileToString(PathToString(asset_path), json_data)) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - material failed to load the json file '{}',", asset_id, PathToString(asset_path)); return {}; } picojson::value root; const auto error = picojson::parse(root, json_data); if (!error.empty()) { ERROR_LOG_FMT( VIDEO, "Asset '{}' error - material failed to load the json file '{}', due to parse error: {}", asset_id, PathToString(asset_path), error); return {}; } if (!root.is()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - material failed to load the json file '{}', due to root not " "being an object!", asset_id, PathToString(asset_path)); return {}; } const auto& root_obj = root.get(); if (!MaterialData::FromJson(asset_id, root_obj, data)) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - material failed to load the json file '{}', as material " "json could not be parsed!", asset_id, PathToString(asset_path)); return {}; } return LoadInfo{json_data.size(), GetLastAssetWriteTime(asset_id)}; } CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMesh(const AssetID& asset_id, MeshData* data) { const auto asset_map = GetAssetMapForID(asset_id); // Asset map for a mesh is the mesh and some metadata if (asset_map.size() != 2) { ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have two files mapped!", asset_id); return {}; } const auto metadata = asset_map.find("metadata"); const auto mesh = asset_map.find("mesh"); if (metadata == asset_map.end()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a metadata entry mapped!", asset_id); return {}; } if (mesh == asset_map.end()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a mesh entry mapped!", asset_id); return {}; } std::size_t metadata_size; { std::error_code ec; metadata_size = std::filesystem::file_size(metadata->second, ec); if (ec) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to get mesh metadata file size with error '{}'!", asset_id, ec); return {}; } } std::size_t mesh_size; { std::error_code ec; mesh_size = std::filesystem::file_size(mesh->second, ec); if (ec) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to get mesh file size with error '{}'!", asset_id, ec); return {}; } } const auto approx_mem_size = metadata_size + mesh_size; File::IOFile file(PathToString(mesh->second), "rb"); if (!file.IsOpen()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to open mesh file '{}'!", asset_id, PathToString(mesh->second)); return {}; } std::vector bytes; bytes.reserve(file.GetSize()); file.ReadBytes(bytes.data(), file.GetSize()); if (!MeshData::FromDolphinMesh(bytes, data)) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the mesh file '{}'!", asset_id, PathToString(mesh->second)); return {}; } std::string json_data; if (!File::ReadFileToString(PathToString(metadata->second), json_data)) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the json file '{}'!", asset_id, PathToString(metadata->second)); return {}; } picojson::value root; const auto error = picojson::parse(root, json_data); if (!error.empty()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the json file '{}', due to parse error: {}", asset_id, PathToString(metadata->second), error); return {}; } if (!root.is()) { ERROR_LOG_FMT( VIDEO, "Asset '{}' error - failed to load the json file '{}', due to root not being an object!", asset_id, PathToString(metadata->second)); return {}; } const auto& root_obj = root.get(); if (!MeshData::FromJson(asset_id, root_obj, data)) return {}; return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)}; } CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id, TextureData* data) { const auto asset_map = GetAssetMapForID(asset_id); // Texture can optionally have a metadata file as well if (asset_map.empty() || asset_map.size() > 2) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - raw texture expected to have one or two files mapped!", asset_id); return {}; } const auto metadata = asset_map.find("metadata"); const auto texture_path = asset_map.find("texture"); if (texture_path == asset_map.end()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a texture entry mapped!", asset_id); return {}; } std::size_t metadata_size = 0; if (metadata != asset_map.end()) { std::error_code ec; metadata_size = std::filesystem::file_size(metadata->second, ec); if (ec) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to get texture metadata file size with error '{}'!", asset_id, ec); return {}; } std::string json_data; if (!File::ReadFileToString(PathToString(metadata->second), json_data)) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the json file '{}',", asset_id, PathToString(metadata->second)); return {}; } picojson::value root; const auto error = picojson::parse(root, json_data); if (!error.empty()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the json file '{}', due to parse error: {}", asset_id, PathToString(metadata->second), error); return {}; } if (!root.is()) { ERROR_LOG_FMT( VIDEO, "Asset '{}' error - failed to load the json file '{}', due to root not being an object!", asset_id, PathToString(metadata->second)); return {}; } const auto& root_obj = root.get(); if (!TextureData::FromJson(asset_id, root_obj, data)) { return {}; } } else { data->m_sampler = RenderState::GetLinearSamplerState(); data->m_type = TextureData::Type::Type_Texture2D; } auto ext = PathToString(texture_path->second.extension()); Common::ToLower(&ext); if (ext == ".dds") { if (!LoadDDSTexture(&data->m_texture, PathToString(texture_path->second))) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load dds texture!", asset_id); return {}; } if (data->m_texture.m_slices.empty()) [[unlikely]] data->m_texture.m_slices.push_back({}); if (!LoadMips(texture_path->second, &data->m_texture.m_slices[0])) return {}; return LoadInfo{GetAssetSize(data->m_texture) + metadata_size, GetLastAssetWriteTime(asset_id)}; } else if (ext == ".png") { // PNG could support more complicated texture types in the future // but for now just error if (data->m_type != TextureData::Type::Type_Texture2D) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - PNG is not supported for texture type '{}'!", asset_id, data->m_type); return {}; } // If we have no slices, create one if (data->m_texture.m_slices.empty()) data->m_texture.m_slices.push_back({}); auto& slice = data->m_texture.m_slices[0]; // If we have no levels, create one to pass into LoadPNGTexture if (slice.m_levels.empty()) slice.m_levels.push_back({}); if (!LoadPNGTexture(&slice.m_levels[0], PathToString(texture_path->second))) { ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load png texture!", asset_id); return {}; } if (!LoadMips(texture_path->second, &slice)) return {}; return LoadInfo{GetAssetSize(data->m_texture) + metadata_size, GetLastAssetWriteTime(asset_id)}; } ERROR_LOG_FMT(VIDEO, "Asset '{}' error - extension '{}' unknown!", asset_id, ext); return {}; } void DirectFilesystemAssetLibrary::SetAssetIDMapData(const AssetID& asset_id, AssetMap asset_path_map) { std::lock_guard lk(m_lock); m_assetid_to_asset_map_path[asset_id] = std::move(asset_path_map); } bool DirectFilesystemAssetLibrary::LoadMips(const std::filesystem::path& asset_path, CustomTextureData::ArraySlice* data) { if (!data) [[unlikely]] return false; std::string path; std::string filename; std::string extension; SplitPath(PathToString(asset_path), &path, &filename, &extension); std::string extension_lower = extension; Common::ToLower(&extension_lower); // Load additional mip levels for (u32 mip_level = static_cast(data->m_levels.size());; mip_level++) { const auto mip_level_filename = filename + fmt::format("_mip{}", mip_level); const auto full_path = path + mip_level_filename + extension; if (!File::Exists(full_path)) return true; VideoCommon::CustomTextureData::ArraySlice::Level level; if (extension_lower == ".dds") { if (!LoadDDSTexture(&level, full_path, mip_level)) { ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename); return false; } } else if (extension_lower == ".png") { if (!LoadPNGTexture(&level, full_path)) { ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename); return false; } } else { ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' has unsupported extension", mip_level_filename); return false; } data->m_levels.push_back(std::move(level)); } return true; } DirectFilesystemAssetLibrary::AssetMap DirectFilesystemAssetLibrary::GetAssetMapForID(const AssetID& asset_id) const { std::lock_guard lk(m_lock); if (auto iter = m_assetid_to_asset_map_path.find(asset_id); iter != m_assetid_to_asset_map_path.end()) { return iter->second; } return {}; } } // namespace VideoCommon