Unify ISOFile (wx) with GameFile (Qt) and put it in UICommon

The original reason I wanted to do this was so that we can replace
the Android-specific code with this in the future, but of course,
just deduplicating between DolphinWX and DolphinQt2 is nice too.

Fixes:

- DolphinQt2 showing the wrong size for split WBFS disc images.

- DolphinQt2 being case sensitive when checking if a file is a DOL/ELF.

- DolphinQt2 not detecting when a Wii banner has become available
after the game list cache was created.

Removes:

- DolphinWX's ability to load PNGs as custom banners. But it was
already rather broken (see https://bugs.dolphin-emu.org/issues/10365
and https://bugs.dolphin-emu.org/issues/10366). The reason I removed
this was because PNG decoding relied on wx code and we don't have any
good non-wx/Qt code for loading PNG files right now (let's not use
SOIL), but we should be able to use libpng directly to implement PNG
loading in the future.

- DolphinQt2's ability to ignore a cached game if the last modified
time differs. We currently don't have a non-wx/Qt way to get the time.
This commit is contained in:
JosJuice
2018-03-09 13:08:38 +01:00
parent 1add238a28
commit 1f1dae367d
67 changed files with 1373 additions and 1736 deletions
+2
View File
@@ -1,6 +1,8 @@
set(SRCS
CommandLineParse.cpp
Disassembler.cpp
GameFile.cpp
GameFileCache.cpp
UICommon.cpp
USBUtils.cpp
VideoUtils.cpp
+312
View File
@@ -0,0 +1,312 @@
// Copyright 2008 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "UICommon/GameFile.h"
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <cstring>
#include <iterator>
#include <map>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "Common/ChunkFile.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
#include "Common/Hash.h"
#include "Common/IniFile.h"
#include "Common/NandPaths.h"
#include "Common/StringUtil.h"
#include "Core/Boot/Boot.h"
#include "Core/ConfigManager.h"
#include "Core/IOS/ES/Formats.h"
#include "Core/TitleDatabase.h"
#include "DiscIO/Blob.h"
#include "DiscIO/Enums.h"
#include "DiscIO/Volume.h"
#include "DiscIO/WiiSaveBanner.h"
namespace UICommon
{
static const std::string EMPTY_STRING;
const std::string& GameFile::Lookup(DiscIO::Language language,
const std::map<DiscIO::Language, std::string>& strings)
{
auto end = strings.end();
auto it = strings.find(language);
if (it != end)
return it->second;
// English tends to be a good fallback when the requested language isn't available
if (language != DiscIO::Language::LANGUAGE_ENGLISH)
{
it = strings.find(DiscIO::Language::LANGUAGE_ENGLISH);
if (it != end)
return it->second;
}
// If English isn't available either, just pick something
if (!strings.empty())
return strings.cbegin()->second;
return EMPTY_STRING;
}
const std::string&
GameFile::LookupUsingConfigLanguage(const std::map<DiscIO::Language, std::string>& strings) const
{
const bool wii = DiscIO::IsWii(m_platform);
return Lookup(SConfig::GetInstance().GetCurrentLanguage(wii), strings);
}
GameFile::GameFile(const std::string& path)
: m_file_path(path), m_region(DiscIO::Region::UNKNOWN_REGION),
m_country(DiscIO::Country::COUNTRY_UNKNOWN)
{
{
std::string name, extension;
SplitPath(m_file_path, nullptr, &name, &extension);
m_file_name = name + extension;
std::unique_ptr<DiscIO::Volume> volume(DiscIO::CreateVolumeFromFilename(m_file_path));
if (volume != nullptr)
{
m_platform = volume->GetVolumeType();
m_short_names = volume->GetShortNames();
m_long_names = volume->GetLongNames();
m_short_makers = volume->GetShortMakers();
m_long_makers = volume->GetLongMakers();
m_descriptions = volume->GetDescriptions();
m_region = volume->GetRegion();
m_country = volume->GetCountry();
m_blob_type = volume->GetBlobType();
m_file_size = volume->GetRawSize();
m_volume_size = volume->GetSize();
m_internal_name = volume->GetInternalName();
m_game_id = volume->GetGameID();
m_title_id = volume->GetTitleID().value_or(0);
m_maker_id = volume->GetMakerID();
m_revision = volume->GetRevision().value_or(0);
m_disc_number = volume->GetDiscNumber().value_or(0);
m_apploader_date = volume->GetApploaderDate();
m_volume_banner.buffer = volume->GetBanner(&m_volume_banner.width, &m_volume_banner.height);
m_valid = true;
}
}
if (!IsValid() && IsElfOrDol())
{
m_valid = true;
m_file_size = File::GetSize(m_file_path);
m_platform = DiscIO::Platform::ELF_DOL;
m_blob_type = DiscIO::BlobType::DIRECTORY;
}
}
bool GameFile::IsValid() const
{
if (!m_valid)
return false;
if (m_platform == DiscIO::Platform::WII_WAD && !IOS::ES::IsChannel(m_title_id))
return false;
return true;
}
bool GameFile::CustomNameChanged(const Core::TitleDatabase& title_database)
{
const auto type = m_platform == DiscIO::Platform::WII_WAD ?
Core::TitleDatabase::TitleType::Channel :
Core::TitleDatabase::TitleType::Other;
m_pending.custom_name = title_database.GetTitleName(m_game_id, type);
return m_custom_name != m_pending.custom_name;
}
void GameFile::CustomNameCommit()
{
m_custom_name = std::move(m_pending.custom_name);
}
bool GameFile::EmuStateChanged()
{
IniFile ini = SConfig::LoadGameIni(m_game_id, m_revision);
ini.GetIfExists("EmuState", "EmulationStateId", &m_pending.emu_state.rating, 0);
ini.GetIfExists("EmuState", "EmulationIssues", &m_pending.emu_state.issues, std::string());
return m_emu_state != m_pending.emu_state;
}
void GameFile::EmuStateCommit()
{
m_emu_state = std::move(m_pending.emu_state);
}
void GameFile::EmuState::DoState(PointerWrap& p)
{
p.Do(rating);
p.Do(issues);
}
void GameBanner::DoState(PointerWrap& p)
{
p.Do(buffer);
p.Do(width);
p.Do(height);
}
void GameFile::DoState(PointerWrap& p)
{
p.Do(m_valid);
p.Do(m_file_path);
p.Do(m_file_name);
p.Do(m_file_size);
p.Do(m_volume_size);
p.Do(m_short_names);
p.Do(m_long_names);
p.Do(m_short_makers);
p.Do(m_long_makers);
p.Do(m_descriptions);
p.Do(m_internal_name);
p.Do(m_game_id);
p.Do(m_title_id);
p.Do(m_maker_id);
p.Do(m_region);
p.Do(m_country);
p.Do(m_platform);
p.Do(m_blob_type);
p.Do(m_revision);
p.Do(m_disc_number);
p.Do(m_apploader_date);
m_volume_banner.DoState(p);
m_emu_state.DoState(p);
p.Do(m_custom_name);
}
bool GameFile::IsElfOrDol() const
{
if (m_file_path.size() < 4)
return false;
std::string name_end = m_file_path.substr(m_file_path.size() - 4);
std::transform(name_end.begin(), name_end.end(), name_end.begin(), ::tolower);
return name_end == ".elf" || name_end == ".dol";
}
bool GameFile::BannerChanged()
{
// Wii banners can only be read if there is a save file.
// In case the cache was created without a save file existing,
// let's try reading the save file again, because it might exist now.
if (!m_volume_banner.empty())
return false;
if (!DiscIO::IsWii(m_platform))
return false;
m_volume_banner.buffer =
DiscIO::WiiSaveBanner(m_title_id).GetBanner(&m_volume_banner.width, &m_volume_banner.height);
if (m_volume_banner.buffer.empty())
return false;
// We only reach here if m_volume_banner was empty, so we can always return true
// without needing any extra check to know whether the banners are different
return true;
}
void GameFile::BannerCommit()
{
m_volume_banner = std::move(m_pending.volume_banner);
}
const std::string& GameFile::GetName(bool long_name) const
{
if (!m_custom_name.empty())
return m_custom_name;
const std::string& name = long_name ? GetLongName() : GetShortName();
if (!name.empty())
return name;
// No usable name, return filename (better than nothing)
return m_file_name;
}
const std::string& GameFile::GetMaker(bool long_maker) const
{
const std::string& maker = long_maker ? GetLongMaker() : GetShortMaker();
if (!maker.empty())
return maker;
if (m_game_id.size() >= 6)
return DiscIO::GetCompanyFromID(m_maker_id);
return EMPTY_STRING;
}
std::vector<DiscIO::Language> GameFile::GetLanguages() const
{
std::vector<DiscIO::Language> languages;
// TODO: What if some languages don't have long names but have other strings?
for (std::pair<DiscIO::Language, std::string> name : m_long_names)
languages.push_back(name.first);
return languages;
}
std::string GameFile::GetUniqueIdentifier() const
{
const DiscIO::Language lang = DiscIO::Language::LANGUAGE_ENGLISH;
std::vector<std::string> info;
if (!GetGameID().empty())
info.push_back(GetGameID());
if (GetRevision() != 0)
info.push_back("Revision " + std::to_string(GetRevision()));
std::string name(GetLongName(lang));
if (name.empty())
name = GetName();
int disc_number = GetDiscNumber() + 1;
std::string lower_name = name;
std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower);
if (disc_number > 1 &&
lower_name.find(StringFromFormat("disc %i", disc_number)) == std::string::npos &&
lower_name.find(StringFromFormat("disc%i", disc_number)) == std::string::npos)
{
std::string disc_text = "Disc ";
info.push_back(disc_text + std::to_string(disc_number));
}
if (info.empty())
return name;
std::ostringstream ss;
std::copy(info.begin(), info.end() - 1, std::ostream_iterator<std::string>(ss, ", "));
ss << info.back();
return name + " (" + ss.str() + ")";
}
std::string GameFile::GetWiiFSPath() const
{
_assert_(DiscIO::IsWii(m_platform));
return Common::GetTitleDataPath(m_title_id, Common::FROM_CONFIGURED_ROOT);
}
} // namespace UICommon
+153
View File
@@ -0,0 +1,153 @@
// Copyright 2008 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <map>
#include <string>
#include <vector>
#include "Common/CommonTypes.h"
class PointerWrap;
namespace Core
{
class TitleDatabase;
}
namespace DiscIO
{
enum class BlobType;
enum class Country;
enum class Language;
enum class Region;
enum class Platform;
}
namespace UICommon
{
struct GameBanner
{
std::vector<u32> buffer{};
int width{};
int height{};
bool empty() const { return buffer.empty(); }
void DoState(PointerWrap& p);
};
// This class caches the metadata of a DiscIO::Volume (or a DOL/ELF file).
class GameFile final
{
public:
GameFile() = default;
explicit GameFile(const std::string& path);
~GameFile() = default;
bool IsValid() const;
const std::string& GetFilePath() const { return m_file_path; }
const std::string& GetFileName() const { return m_file_name; }
const std::string& GetName(bool long_name = true) const;
const std::string& GetMaker(bool long_maker = true) const;
const std::string& GetShortName(DiscIO::Language l) const { return Lookup(l, m_short_names); }
const std::string& GetShortName() const { return LookupUsingConfigLanguage(m_short_names); }
const std::string& GetLongName(DiscIO::Language l) const { return Lookup(l, m_long_names); }
const std::string& GetLongName() const { return LookupUsingConfigLanguage(m_long_names); }
const std::string& GetShortMaker(DiscIO::Language l) const { return Lookup(l, m_short_makers); }
const std::string& GetShortMaker() const { return LookupUsingConfigLanguage(m_short_makers); }
const std::string& GetLongMaker(DiscIO::Language l) const { return Lookup(l, m_long_makers); }
const std::string& GetLongMaker() const { return LookupUsingConfigLanguage(m_long_makers); }
const std::string& GetDescription(DiscIO::Language l) const { return Lookup(l, m_descriptions); }
const std::string& GetDescription() const { return LookupUsingConfigLanguage(m_descriptions); }
std::vector<DiscIO::Language> GetLanguages() const;
const std::string& GetInternalName() const { return m_internal_name; }
const std::string& GetGameID() const { return m_game_id; }
u64 GetTitleID() const { return m_title_id; }
const std::string& GetMakerID() const { return m_maker_id; }
u16 GetRevision() const { return m_revision; }
// 0 is the first disc, 1 is the second disc
u8 GetDiscNumber() const { return m_disc_number; }
std::string GetUniqueIdentifier() const;
std::string GetWiiFSPath() const;
DiscIO::Region GetRegion() const { return m_region; }
DiscIO::Country GetCountry() const { return m_country; }
DiscIO::Platform GetPlatform() const { return m_platform; }
DiscIO::BlobType GetBlobType() const { return m_blob_type; }
const std::string& GetApploaderDate() const { return m_apploader_date; }
const std::string& GetIssues() const { return m_emu_state.issues; }
int GetEmuState() const { return m_emu_state.rating; }
u64 GetFileSize() const { return m_file_size; }
u64 GetVolumeSize() const { return m_volume_size; }
const GameBanner& GetBannerImage() const { return m_volume_banner; }
void DoState(PointerWrap& p);
bool BannerChanged();
void BannerCommit();
bool EmuStateChanged();
void EmuStateCommit();
bool CustomNameChanged(const Core::TitleDatabase& title_database);
void CustomNameCommit();
private:
struct EmuState
{
int rating{};
std::string issues{};
bool operator!=(const EmuState& rhs) const
{
return rating != rhs.rating || issues != rhs.issues;
}
void DoState(PointerWrap& p);
};
static const std::string& Lookup(DiscIO::Language language,
const std::map<DiscIO::Language, std::string>& strings);
const std::string&
LookupUsingConfigLanguage(const std::map<DiscIO::Language, std::string>& strings) const;
bool IsElfOrDol() const;
// IMPORTANT: Nearly all data members must be save/restored in DoState.
// If anything is changed, make sure DoState handles it properly and
// CACHE_REVISION in GameFileCache.cpp is incremented.
bool m_valid{};
std::string m_file_path{};
std::string m_file_name{};
u64 m_file_size{};
u64 m_volume_size{};
std::map<DiscIO::Language, std::string> m_short_names{};
std::map<DiscIO::Language, std::string> m_long_names{};
std::map<DiscIO::Language, std::string> m_short_makers{};
std::map<DiscIO::Language, std::string> m_long_makers{};
std::map<DiscIO::Language, std::string> m_descriptions{};
std::string m_internal_name{};
std::string m_game_id{};
u64 m_title_id{};
std::string m_maker_id{};
DiscIO::Region m_region{};
DiscIO::Country m_country{};
DiscIO::Platform m_platform{};
DiscIO::BlobType m_blob_type{};
u16 m_revision{};
u8 m_disc_number{};
std::string m_apploader_date{};
GameBanner m_volume_banner{};
EmuState m_emu_state{};
// Overridden name from TitleDatabase
std::string m_custom_name{};
// The following data members allow GameFileCache to construct updated versions
// of GameFiles in a threadsafe way. They should not be handled in DoState.
struct
{
EmuState emu_state;
GameBanner volume_banner;
std::string custom_name;
} m_pending{};
};
} // namespace UICommon
+235
View File
@@ -0,0 +1,235 @@
// Copyright 2017 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "UICommon/GameFileCache.h"
#include <algorithm>
#include <cstddef>
#include <functional>
#include <list>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_set>
#include <vector>
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/File.h"
#include "Common/FileSearch.h"
#include "Common/FileUtil.h"
#include "Core/TitleDatabase.h"
#include "DiscIO/DirectoryBlob.h"
#include "UICommon/GameFile.h"
namespace UICommon
{
static constexpr u32 CACHE_REVISION = 7; // Last changed in PR 6281
std::vector<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
bool recursive_scan)
{
static const std::vector<std::string> search_extensions = {
".gcm", ".tgc", ".iso", ".ciso", ".gcz", ".wbfs", ".wad", ".dol", ".elf"};
// TODO: We could process paths iteratively as they are found
return Common::DoFileSearch(directories_to_scan, search_extensions, recursive_scan);
}
void GameFileCache::ForEach(std::function<void(const std::shared_ptr<const GameFile>&)> f) const
{
for (const std::shared_ptr<const GameFile>& item : m_cached_files)
f(item);
}
void GameFileCache::Clear()
{
m_cached_files.clear();
}
std::shared_ptr<const GameFile> GameFileCache::AddOrGet(const std::string& path,
bool* cache_changed,
const Core::TitleDatabase& title_database)
{
auto it = std::find_if(
m_cached_files.begin(), m_cached_files.end(),
[&path](const std::shared_ptr<GameFile>& file) { return file->GetFilePath() == path; });
const bool found = it != m_cached_files.cend();
if (!found)
m_cached_files.emplace_back(std::make_shared<GameFile>(path));
std::shared_ptr<GameFile>& result = found ? *it : m_cached_files.back();
if (UpdateAdditionalMetadata(&result, title_database) || !found)
*cache_changed = true;
return result;
}
bool GameFileCache::Update(const std::vector<std::string>& all_game_paths)
{
// Copy game paths into a set, except ones that match DiscIO::ShouldHideFromGameList.
// TODO: Prevent DoFileSearch from looking inside /files/ directories of DirectoryBlobs at all?
// TODO: Make DoFileSearch support filter predicates so we don't have remove things afterwards?
std::unordered_set<std::string> game_paths;
game_paths.reserve(all_game_paths.size());
for (const std::string& path : all_game_paths)
{
if (!DiscIO::ShouldHideFromGameList(path))
game_paths.insert(path);
}
bool cache_changed = false;
// Delete paths that aren't in game_paths from m_cached_files,
// while simultaneously deleting paths that aren't in m_cached_files from game_paths.
// For the sake of speed, we don't care about maintaining the order of m_cached_files.
{
auto it = m_cached_files.begin();
auto end = m_cached_files.end();
while (it != end)
{
if (game_paths.erase((*it)->GetFilePath()))
{
++it;
}
else
{
cache_changed = true;
--end;
*it = std::move(*end);
}
}
m_cached_files.erase(it, m_cached_files.end());
}
// Now that the previous loop has run, game_paths only contains paths that
// aren't in m_cached_files, so we simply add all of them to m_cached_files.
for (const auto& path : game_paths)
{
auto file = std::make_shared<GameFile>(path);
if (file->IsValid())
{
cache_changed = true;
m_cached_files.push_back(std::move(file));
}
}
return cache_changed;
}
bool GameFileCache::UpdateAdditionalMetadata(const Core::TitleDatabase& title_database)
{
bool cache_changed = false;
for (auto& file : m_cached_files)
cache_changed |= UpdateAdditionalMetadata(&file, title_database);
return cache_changed;
}
bool GameFileCache::UpdateAdditionalMetadata(std::shared_ptr<GameFile>* game_file,
const Core::TitleDatabase& title_database)
{
const bool emu_state_changed = (*game_file)->EmuStateChanged();
const bool banner_changed = (*game_file)->BannerChanged();
const bool custom_title_changed = (*game_file)->CustomNameChanged(title_database);
if (!emu_state_changed && !banner_changed && !custom_title_changed)
return false;
// If a cached file needs an update, apply the updates to a copy and delete the original.
// This makes the usage of cached files in other threads safe.
std::shared_ptr<GameFile> copy = std::make_shared<GameFile>(**game_file);
if (emu_state_changed)
copy->EmuStateCommit();
if (banner_changed)
copy->BannerCommit();
if (custom_title_changed)
copy->CustomNameCommit();
*game_file = std::move(copy);
return true;
}
bool GameFileCache::Load()
{
return SyncCacheFile(false);
}
bool GameFileCache::Save()
{
return SyncCacheFile(true);
}
bool GameFileCache::SyncCacheFile(bool save)
{
std::string filename(File::GetUserPath(D_CACHE_IDX) + "gamelist.cache");
const char* open_mode = save ? "wb" : "rb";
File::IOFile f(filename, open_mode);
if (!f)
return false;
bool success = false;
if (save)
{
// Measure the size of the buffer.
u8* ptr = nullptr;
PointerWrap p(&ptr, PointerWrap::MODE_MEASURE);
DoState(&p);
const size_t buffer_size = reinterpret_cast<size_t>(ptr);
// Then actually do the write.
std::vector<u8> buffer(buffer_size);
ptr = &buffer[0];
p.SetMode(PointerWrap::MODE_WRITE);
DoState(&p, buffer_size);
if (f.WriteBytes(buffer.data(), buffer.size()))
success = true;
}
else
{
std::vector<u8> buffer(f.GetSize());
if (buffer.size() && f.ReadBytes(buffer.data(), buffer.size()))
{
u8* ptr = buffer.data();
PointerWrap p(&ptr, PointerWrap::MODE_READ);
DoState(&p, buffer.size());
if (p.GetMode() == PointerWrap::MODE_READ)
success = true;
}
}
if (!success)
{
// If some file operation failed, try to delete the probably-corrupted cache
f.Close();
File::Delete(filename);
}
return success;
}
void GameFileCache::DoState(PointerWrap* p, u64 size)
{
struct
{
u32 revision;
u64 expected_size;
} header = {CACHE_REVISION, size};
p->Do(header);
if (p->GetMode() == PointerWrap::MODE_READ)
{
if (header.revision != CACHE_REVISION || header.expected_size != size)
{
p->SetMode(PointerWrap::MODE_MEASURE);
return;
}
}
p->DoEachElement(m_cached_files, [](PointerWrap& state, std::shared_ptr<GameFile>& elem) {
if (state.GetMode() == PointerWrap::MODE_READ)
elem = std::make_shared<GameFile>();
elem->DoState(state);
});
}
} // namespace DiscIO
+58
View File
@@ -0,0 +1,58 @@
// Copyright 2017 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <cstddef>
#include <functional>
#include <list>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "Common/CommonTypes.h"
class PointerWrap;
namespace Core
{
class TitleDatabase;
}
namespace UICommon
{
class GameFile;
std::vector<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
bool recursive_scan);
class GameFileCache
{
public:
void ForEach(std::function<void(const std::shared_ptr<const GameFile>&)> f) const;
void Clear();
std::shared_ptr<const GameFile> AddOrGet(const std::string& path, bool* cache_changed,
const Core::TitleDatabase& title_database);
// These functions return true if the call modified the cache.
bool Update(const std::vector<std::string>& all_game_paths);
bool UpdateAdditionalMetadata(const Core::TitleDatabase& title_database);
bool Load();
bool Save();
private:
bool UpdateAdditionalMetadata(std::shared_ptr<GameFile>* game_file,
const Core::TitleDatabase& title_database);
bool SyncCacheFile(bool save);
void DoState(PointerWrap* p, u64 size = 0);
std::vector<std::shared_ptr<GameFile>> m_cached_files;
};
} // namespace UICommon
+21
View File
@@ -2,16 +2,20 @@
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include <cmath>
#include <memory>
#ifdef _WIN32
#include <shlobj.h> // for SHGetFolderPath
#endif
#include "Common/Common.h"
#include "Common/CommonPaths.h"
#include "Common/Config/Config.h"
#include "Common/FileUtil.h"
#include "Common/Logging/LogManager.h"
#include "Common/MathUtil.h"
#include "Common/MsgHandler.h"
#include "Common/StringUtil.h"
#include "Core/ConfigLoaders/BaseConfigLoader.h"
#include "Core/ConfigManager.h"
@@ -313,4 +317,21 @@ void EnableScreenSaver(bool enable)
#endif
}
std::string FormatSize(u64 bytes)
{
// i18n: The symbol for the unit "bytes"
const char* const unit_symbols[] = {_trans("B"), _trans("KiB"), _trans("MiB"), _trans("GiB"),
_trans("TiB"), _trans("PiB"), _trans("EiB")};
// Find largest power of 2 less than size.
// div 10 to get largest named unit less than size
// 10 == log2(1024) (number of B in a KiB, KiB in a MiB, etc)
// Max value is 63 / 10 = 6
const int unit = IntLog2(std::max<u64>(bytes, 1)) / 10;
// Don't need exact values, only 5 most significant digits
const double unit_size = std::pow(2, unit * 10);
return StringFromFormat("%.2f %s", bytes / unit_size, GetStringT(unit_symbols[unit]).c_str());
}
} // namespace UICommon
+4
View File
@@ -25,4 +25,8 @@ void SetUserDirectory(const std::string& custom_path);
bool TriggerSTMPowerEvent();
void SaveWiimoteSources();
// Return a pretty file size string from byte count.
// e.g. 1134278 -> "1.08 MiB"
std::string FormatSize(u64 bytes);
} // namespace UICommon
+4
View File
@@ -51,12 +51,16 @@
<DisableSpecificWarnings>4200;%(DisableSpecificWarnings)</DisableSpecificWarnings>
</ClCompile>
<ClCompile Include="VideoUtils.cpp" />
<ClCompile Include="GameFile.cpp" />
<ClCompile Include="GameFileCache.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="CommandLineParse.h" />
<ClInclude Include="UICommon.h" />
<ClInclude Include="Disassembler.h" />
<ClInclude Include="USBUtils.h" />
<ClInclude Include="GameFile.h" />
<ClInclude Include="GameFileCache.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(ExternalsDir)cpp-optparse\cpp-optparse.vcxproj">