DolphinWX: defer gamelist scanning and switch to single-file cache.

This commit is contained in:
Shawn Hoffman
2017-06-20 16:36:59 -07:00
parent 79961b6f76
commit f16599f4a8
12 changed files with 378 additions and 393 deletions

View File

@ -44,6 +44,7 @@
#include "Common/MathUtil.h"
#include "Common/StringUtil.h"
#include "Common/SysConf.h"
#include "Common/Thread.h"
#include "Core/Boot/Boot.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
@ -155,36 +156,6 @@ static int CompareGameListItems(const GameListItem* iso1, const GameListItem* is
return 0;
}
static std::vector<std::string> GetFileSearchExtensions()
{
std::vector<std::string> extensions;
if (SConfig::GetInstance().m_ListGC)
{
extensions.push_back(".gcm");
extensions.push_back(".tgc");
}
if (SConfig::GetInstance().m_ListWii || SConfig::GetInstance().m_ListGC)
{
extensions.push_back(".iso");
extensions.push_back(".ciso");
extensions.push_back(".gcz");
extensions.push_back(".wbfs");
}
if (SConfig::GetInstance().m_ListWad)
extensions.push_back(".wad");
if (SConfig::GetInstance().m_ListElfDol)
{
extensions.push_back(".dol");
extensions.push_back(".elf");
}
return extensions;
}
static bool ShouldDisplayGameListItem(const GameListItem& item)
{
const bool show_platform = [&item] {
@ -240,7 +211,8 @@ static bool ShouldDisplayGameListItem(const GameListItem& item)
}
}
wxDEFINE_EVENT(DOLPHIN_EVT_RELOAD_GAMELIST, wxCommandEvent);
wxDEFINE_EVENT(DOLPHIN_EVT_REFRESH_GAMELIST, wxCommandEvent);
wxDEFINE_EVENT(DOLPHIN_EVT_RESCAN_GAMELIST, wxCommandEvent);
struct GameListCtrl::ColumnInfo
{
@ -250,8 +222,8 @@ struct GameListCtrl::ColumnInfo
bool& visible;
};
GameListCtrl::GameListCtrl(wxWindow* parent, const wxWindowID id, const wxPoint& pos,
const wxSize& size, long style)
GameListCtrl::GameListCtrl(bool disable_scanning, wxWindow* parent, const wxWindowID id,
const wxPoint& pos, const wxSize& size, long style)
: wxListCtrl(parent, id, pos, size, style), m_tooltip(nullptr),
m_columns({// {COLUMN, {default_width (without platform padding), resizability, visibility}}
{COLUMN_PLATFORM, 32 + 1 /* icon padding */, false,
@ -286,9 +258,57 @@ GameListCtrl::GameListCtrl(wxWindow* parent, const wxWindowID id, const wxPoint&
Bind(wxEVT_MENU, &GameListCtrl::OnChangeDisc, this, IDM_LIST_CHANGE_DISC);
Bind(wxEVT_MENU, &GameListCtrl::OnNetPlayHost, this, IDM_START_NETPLAY);
Bind(DOLPHIN_EVT_RELOAD_GAMELIST, &GameListCtrl::OnReloadGameList, this);
Bind(DOLPHIN_EVT_REFRESH_GAMELIST, &GameListCtrl::OnRefreshGameList, this);
Bind(DOLPHIN_EVT_RESCAN_GAMELIST, &GameListCtrl::OnRescanGameList, this);
wxTheApp->Bind(DOLPHIN_EVT_LOCAL_INI_CHANGED, &GameListCtrl::OnLocalIniModified, this);
if (!disable_scanning)
{
m_scan_thread = std::thread([&] {
Common::SetCurrentThreadName("gamelist scanner");
if (SyncCacheFile(false))
{
// Account for changes outside the cache (just wii banners atm; could scan Ini here)
// Note this is on the initial load path. Further improvements could be made if updates
// from scan->ui threads described small changelists instead of updating the entire list
// at once.
bool cache_modified = false;
for (auto& file : m_cached_files)
{
if (file->ReloadBannerIfNeeded())
cache_modified = true;
}
QueueEvent(new wxCommandEvent(DOLPHIN_EVT_REFRESH_GAMELIST));
if (cache_modified)
SyncCacheFile(true);
}
else
{
RescanList();
}
m_scan_trigger.Wait();
while (!m_scan_exiting.IsSet())
{
RescanList();
m_scan_trigger.Wait();
}
});
}
}
GameListCtrl::~GameListCtrl()
{
if (m_scan_thread.joinable())
{
m_scan_exiting.Set();
m_scan_trigger.Set();
m_scan_thread.join();
}
}
template <typename T>
@ -345,8 +365,8 @@ void GameListCtrl::InitBitmaps()
auto& platform_indexes = m_image_indexes.platform;
platform_indexes.resize(static_cast<size_t>(DiscIO::Platform::NUMBER_OF_PLATFORMS));
InitBitmap(img_list, &platform_indexes, this, platform_bmp_size,
DiscIO::Platform::GAMECUBE_DISC, "Platform_Gamecube");
InitBitmap(img_list, &platform_indexes, this, platform_bmp_size, DiscIO::Platform::GAMECUBE_DISC,
"Platform_Gamecube");
InitBitmap(img_list, &platform_indexes, this, platform_bmp_size, DiscIO::Platform::WII_DISC,
"Platform_Wii");
InitBitmap(img_list, &platform_indexes, this, platform_bmp_size, DiscIO::Platform::WII_WAD,
@ -388,24 +408,42 @@ void GameListCtrl::BrowseForDirectory()
{
SConfig::GetInstance().m_ISOFolder.push_back(sPath);
SConfig::GetInstance().SaveSettings();
ReloadList();
m_scan_trigger.Set();
}
}
}
void GameListCtrl::ReloadList()
void GameListCtrl::RefreshList()
{
int scrollPos = wxWindow::GetScrollPos(wxVERTICAL);
// Don't let the user refresh it while a game is running
if (Core::GetState() != Core::State::Uninitialized)
return;
ScanForISOs();
m_shown_files.clear();
for (auto& item : m_cached_files)
{
if (ShouldDisplayGameListItem(*item))
m_shown_files.push_back(item);
}
// Drives are not cached. Not sure if this is required, but better to err on the
// side of caution if cross-platform issues could come into play.
if (SConfig::GetInstance().m_ListDrives)
{
const Core::TitleDatabase title_database;
for (const auto& drive : cdio_get_devices())
{
auto file = std::make_shared<GameListItem>(drive, title_database);
if (file->IsValid())
m_shown_files.push_back(file);
}
}
Freeze();
ClearAll();
if (m_ISOFiles.size() != 0)
if (!m_shown_files.empty())
{
// Don't load bitmaps unless there are games to list
InitBitmaps();
@ -430,14 +468,13 @@ void GameListCtrl::ReloadList()
#endif
// set initial sizes for columns
SetColumnWidth(COLUMN_DUMMY, 0);
for (const auto& c : m_columns)
{
SetColumnWidth(c.id, c.visible ? FromDIP(c.default_width + platform_padding) : 0);
}
// add all items
for (int i = 0; i < (int)m_ISOFiles.size(); i++)
for (int i = 0; i < (int)m_shown_files.size(); i++)
InsertItemInReportView(i);
SetColors();
@ -511,7 +548,7 @@ static wxString NiceSizeFormat(u64 size)
return wxString::Format("%.2f %s", size / unit_size, unit_symbols[unit]);
}
// Update the column content of the item at _Index
// Update the column content of the item at index
void GameListCtrl::UpdateItemAtColumn(long index, int column)
{
const auto& iso_file = *GetISO(GetItemData(index));
@ -521,7 +558,7 @@ void GameListCtrl::UpdateItemAtColumn(long index, int column)
case COLUMN_PLATFORM:
{
SetItemColumnImage(index, COLUMN_PLATFORM,
m_image_indexes.platform[static_cast<size_t>(iso_file.GetPlatform())]);
m_image_indexes.platform[static_cast<size_t>(iso_file.GetPlatform())]);
break;
}
case COLUMN_BANNER:
@ -565,7 +602,7 @@ void GameListCtrl::UpdateItemAtColumn(long index, int column)
break;
case COLUMN_COUNTRY:
SetItemColumnImage(index, COLUMN_COUNTRY,
m_image_indexes.flag[static_cast<size_t>(iso_file.GetCountry())]);
m_image_indexes.flag[static_cast<size_t>(iso_file.GetCountry())]);
break;
case COLUMN_SIZE:
SetItem(index, COLUMN_SIZE, NiceSizeFormat(iso_file.GetFileSize()), -1);
@ -631,96 +668,148 @@ void GameListCtrl::SetColors()
}
}
void GameListCtrl::ScanForISOs()
void GameListCtrl::DoState(PointerWrap* p, u32 size)
{
m_ISOFiles.clear();
const Core::TitleDatabase title_database;
auto rFilenames =
Common::DoFileSearch(GetFileSearchExtensions(), SConfig::GetInstance().m_ISOFolder,
SConfig::GetInstance().m_RecursiveISOFolder);
if (rFilenames.size() > 0)
struct
{
wxProgressDialog dialog(
_("Scanning for ISOs"), _("Scanning..."), (int)rFilenames.size() - 1, this,
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT | wxPD_ELAPSED_TIME | wxPD_ESTIMATED_TIME |
wxPD_REMAINING_TIME | wxPD_SMOOTH // - makes updates as small as possible (down to 1px)
);
for (u32 i = 0; i < rFilenames.size(); i++)
u32 Revision;
u32 ExpectedSize;
} header = {CACHE_REVISION, size};
p->Do(header);
if (p->GetMode() == PointerWrap::MODE_READ)
{
if (header.Revision != CACHE_REVISION || header.ExpectedSize != size)
{
std::string FileName;
SplitPath(rFilenames[i], nullptr, &FileName, nullptr);
// Update with the progress (i) and the message
dialog.Update(i, wxString::Format(_("Scanning %s"), StrToWxStr(FileName)));
if (dialog.WasCancelled())
break;
auto iso_file = std::make_unique<GameListItem>(rFilenames[i], title_database);
if (iso_file->IsValid() && ShouldDisplayGameListItem(*iso_file))
{
m_ISOFiles.push_back(std::move(iso_file));
}
p->SetMode(PointerWrap::MODE_MEASURE);
return;
}
}
if (SConfig::GetInstance().m_ListDrives)
{
const std::vector<std::string> drives = cdio_get_devices();
for (const auto& drive : drives)
p->DoEachElement(m_cached_files, [](PointerWrap& state, std::shared_ptr<GameListItem>& elem) {
if (state.GetMode() == PointerWrap::MODE_READ)
{
auto gli = std::make_unique<GameListItem>(drive, title_database);
if (gli->IsValid())
m_ISOFiles.push_back(std::move(gli));
elem = std::make_shared<GameListItem>();
}
}
std::sort(m_ISOFiles.begin(), m_ISOFiles.end());
elem->DoState(state);
});
}
void GameListCtrl::OnReloadGameList(wxCommandEvent& WXUNUSED(event))
bool GameListCtrl::SyncCacheFile(bool write)
{
ReloadList();
std::string filename(File::GetUserPath(D_CACHE_IDX) + "wx_gamelist.cache");
const char* open_mode = write ? "wb" : "rb";
File::IOFile f(filename, open_mode);
if (!f)
return false;
bool success = false;
if (write)
{
// 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 GameListCtrl::RescanList()
{
const std::vector<std::string> search_extensions = {".gcm", ".tgc", ".iso", ".ciso", ".gcz",
".wbfs", ".wad", ".dol", ".elf"};
// TODO This could process paths iteratively as they are found
auto search_results = Common::DoFileSearch(search_extensions, SConfig::GetInstance().m_ISOFolder,
SConfig::GetInstance().m_RecursiveISOFolder);
// TODO rethink some good algorithms to use here
std::vector<std::string> cached_paths;
for (const auto& file : m_cached_files)
cached_paths.emplace_back(file->GetFileName());
std::sort(cached_paths.begin(), cached_paths.end());
std::list<std::string> removed_paths;
std::set_difference(cached_paths.cbegin(), cached_paths.cend(), search_results.cbegin(),
search_results.cend(), std::back_inserter(removed_paths));
std::vector<std::string> new_paths;
std::set_difference(search_results.cbegin(), search_results.cend(), cached_paths.cbegin(),
cached_paths.cend(), std::back_inserter(new_paths));
const Core::TitleDatabase title_database;
// TODO we could store all paths and modification times to judge if file needs to be rescanned.
// If we cached paths that turned out to be invalid, this would save failing on them each time
// refresh is done.
// However if people e.g. set dolphin to recursively scan the root of their drive(s), then we
// would cache way too much data. Maybe just use an upper bound of invalid paths to cache?
// For now, only scan new_paths. This could cause false negatives (file actively being written),
// but otherwise
// should be fine.
for (const auto& path : removed_paths)
{
auto it = std::find_if(
m_cached_files.cbegin(), m_cached_files.cend(),
[&path](const std::shared_ptr<GameListItem>& file) { return file->GetFileName() == path; });
if (it != m_cached_files.cend())
m_cached_files.erase(it);
}
for (const auto& path : new_paths)
{
auto file = std::make_shared<GameListItem>(path, title_database);
if (file->IsValid())
m_cached_files.push_back(std::move(file));
}
for (auto& file : m_cached_files)
file->ReloadINI();
// Post UI event to update the displayed list
QueueEvent(new wxCommandEvent(DOLPHIN_EVT_REFRESH_GAMELIST));
SyncCacheFile(true);
}
void GameListCtrl::OnRefreshGameList(wxCommandEvent& WXUNUSED(event))
{
RefreshList();
}
void GameListCtrl::OnRescanGameList(wxCommandEvent& WXUNUSED(event))
{
m_scan_trigger.Set();
}
void GameListCtrl::OnLocalIniModified(wxCommandEvent& ev)
{
ev.Skip();
std::string game_id = WxStrToStr(ev.GetString());
// NOTE: The same game may occur multiple times if there are multiple
// physical copies in the search paths.
for (std::size_t i = 0; i < m_ISOFiles.size(); ++i)
{
if (m_ISOFiles[i]->GetGameID() != game_id)
continue;
m_ISOFiles[i]->ReloadINI();
// The indexes in m_ISOFiles and the list do not line up.
// We need to find the corresponding item in the list (if it exists)
long item_id = 0;
for (; item_id < GetItemCount(); ++item_id)
{
if (i == static_cast<std::size_t>(GetItemData(item_id)))
break;
}
// If the item is not currently being displayed then we're done.
if (item_id == GetItemCount())
continue;
// Update all the columns
for (int j = FIRST_COLUMN_WITH_CONTENT; j < NUMBER_OF_COLUMN; ++j)
{
// NOTE: Banner is not modified by the INI and updating it will
// duplicate it in memory which is not wanted.
if (j != COLUMN_BANNER && GetColumnWidth(j) != 0)
UpdateItemAtColumn(item_id, j);
}
}
// We need show any changes to the ini which could impact our columns. Currently only the
// EmuState/Issues settings can do that. We also need to persist the changes to the cache - so
// just trigger a rescan which will sync the cache and then display the new values.
m_scan_trigger.Set();
}
void GameListCtrl::OnColBeginDrag(wxListEvent& event)
@ -733,8 +822,8 @@ void GameListCtrl::OnColBeginDrag(wxListEvent& event)
const GameListItem* GameListCtrl::GetISO(size_t index) const
{
if (index < m_ISOFiles.size())
return m_ISOFiles[index].get();
if (index < m_shown_files.size())
return m_shown_files[index].get();
return nullptr;
}
@ -790,7 +879,7 @@ void GameListCtrl::OnKeyPress(wxListEvent& event)
static int lastKey = 0, sLoop = 0;
int Loop = 0;
for (int i = 0; i < (int)m_ISOFiles.size(); i++)
for (int i = 0; i < (int)m_shown_files.size(); i++)
{
// Easy way to get game string
wxListItem bleh;
@ -806,7 +895,7 @@ void GameListCtrl::OnKeyPress(wxListEvent& event)
if (lastKey == event.GetKeyCode() && Loop < sLoop)
{
Loop++;
if (i + 1 == (int)m_ISOFiles.size())
if (i + 1 == (int)m_shown_files.size())
i = -1;
continue;
}
@ -827,7 +916,7 @@ void GameListCtrl::OnKeyPress(wxListEvent& event)
// If we get past the last game in the list,
// we'll have to go back to the first one.
if (i + 1 == (int)m_ISOFiles.size() && sLoop > 0 && Loop > 0)
if (i + 1 == (int)m_shown_files.size() && sLoop > 0 && Loop > 0)
i = -1;
}
@ -900,7 +989,7 @@ void GameListCtrl::OnMouseMotion(wxMouseEvent& event)
// Convert to screen coordinates
ClientToScreen(&mx, &my);
m_tooltip->SetBoundingRect(wxRect(mx - GetColumnWidth(COLUMN_EMULATION_STATE), my,
GetColumnWidth(COLUMN_EMULATION_STATE), Rect.GetHeight()));
GetColumnWidth(COLUMN_EMULATION_STATE), Rect.GetHeight()));
m_tooltip->SetPosition(
wxPoint(mx - GetColumnWidth(COLUMN_EMULATION_STATE), my - 5 + Rect.GetHeight()));
lastItem = item;
@ -1044,7 +1133,7 @@ void GameListCtrl::OnRightClick(wxMouseEvent& event)
const GameListItem* GameListCtrl::GetSelectedISO() const
{
if (m_ISOFiles.empty())
if (m_shown_files.empty())
return nullptr;
if (GetSelectedItemCount() == 0)
@ -1143,7 +1232,7 @@ void GameListCtrl::OnDeleteISO(wxCommandEvent& WXUNUSED(event))
{
for (const GameListItem* iso : GetAllSelectedISOs())
File::Delete(iso->GetFileName());
ReloadList();
m_scan_trigger.Set();
}
}
@ -1315,7 +1404,7 @@ void GameListCtrl::CompressSelection(bool _compress)
if (!all_good)
WxUtils::ShowErrorDialog(_("Dolphin was unable to complete the requested action."));
ReloadList();
m_scan_trigger.Set();
}
bool GameListCtrl::CompressCB(const std::string& text, float percent, void* arg)
@ -1388,7 +1477,7 @@ void GameListCtrl::OnCompressISO(wxCommandEvent& WXUNUSED(event))
if (!all_good)
WxUtils::ShowErrorDialog(_("Dolphin was unable to complete the requested action."));
ReloadList();
m_scan_trigger.Set();
}
void GameListCtrl::OnChangeDisc(wxCommandEvent& WXUNUSED(event))