early-access version 3949

This commit is contained in:
pineappleEA 2023-10-26 01:19:00 +02:00
parent e1e4fc0bdc
commit c50160452e
16 changed files with 11045 additions and 91 deletions

View file

@ -291,6 +291,7 @@ find_package(lz4 REQUIRED)
find_package(nlohmann_json 3.8 REQUIRED) find_package(nlohmann_json 3.8 REQUIRED)
find_package(Opus 1.3 MODULE) find_package(Opus 1.3 MODULE)
find_package(RenderDoc MODULE) find_package(RenderDoc MODULE)
find_package(stb MODULE)
find_package(VulkanMemoryAllocator CONFIG) find_package(VulkanMemoryAllocator CONFIG)
find_package(ZLIB 1.2 REQUIRED) find_package(ZLIB 1.2 REQUIRED)
find_package(zstd 1.5 REQUIRED) find_package(zstd 1.5 REQUIRED)

31
CMakeModules/Findstb.cmake Executable file
View file

@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: 2023 Alexandre Bouvier <contact@amb.tf>
#
# SPDX-License-Identifier: GPL-3.0-or-later
find_path(stb_image_INCLUDE_DIR stb_image.h PATH_SUFFIXES stb)
find_path(stb_image_resize_INCLUDE_DIR stb_image_resize.h PATH_SUFFIXES stb)
find_path(stb_image_write_INCLUDE_DIR stb_image_write.h PATH_SUFFIXES stb)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(stb
REQUIRED_VARS
stb_image_INCLUDE_DIR
stb_image_resize_INCLUDE_DIR
stb_image_write_INCLUDE_DIR
)
if (stb_FOUND AND NOT TARGET stb::headers)
add_library(stb::headers INTERFACE IMPORTED)
set_property(TARGET stb::headers PROPERTY
INTERFACE_INCLUDE_DIRECTORIES
"${stb_image_INCLUDE_DIR}"
"${stb_image_resize_INCLUDE_DIR}"
"${stb_image_write_INCLUDE_DIR}"
)
endif()
mark_as_advanced(
stb_image_INCLUDE_DIR
stb_image_resize_INCLUDE_DIR
stb_image_write_INCLUDE_DIR
)

View file

@ -1,7 +1,7 @@
yuzu emulator early access yuzu emulator early access
============= =============
This is the source code for early-access 3948. This is the source code for early-access 3949.
## Legal Notice ## Legal Notice

View file

@ -134,6 +134,10 @@ endif()
# Opus # Opus
if (NOT TARGET Opus::opus) if (NOT TARGET Opus::opus)
set(OPUS_BUILD_TESTING OFF)
set(OPUS_BUILD_PROGRAMS OFF)
set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF)
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF)
add_subdirectory(opus) add_subdirectory(opus)
endif() endif()
@ -168,9 +172,13 @@ if (NOT TARGET LLVM::Demangle)
add_library(LLVM::Demangle ALIAS demangle) add_library(LLVM::Demangle ALIAS demangle)
endif() endif()
add_library(stb stb/stb_dxt.cpp stb/stb_image.cpp stb/stb_image_resize.cpp stb/stb_image_write.cpp) add_library(stb stb/stb_dxt.cpp)
target_include_directories(stb PUBLIC ./stb) target_include_directories(stb PUBLIC ./stb)
if (NOT TARGET stb::headers)
add_library(stb::headers ALIAS stb)
endif()
add_library(bc_decoder bc_decoder/bc_decoder.cpp) add_library(bc_decoder bc_decoder/bc_decoder.cpp)
target_include_directories(bc_decoder PUBLIC ./bc_decoder) target_include_directories(bc_decoder PUBLIC ./bc_decoder)

View file

@ -49,11 +49,6 @@ if (MINGW OR (${CMAKE_SYSTEM_NAME} MATCHES "Linux") OR APPLE)
set(LIBUSB_INCLUDE_DIRS "${LIBUSB_SRC_DIR}/libusb" CACHE PATH "libusb headers path" FORCE) set(LIBUSB_INCLUDE_DIRS "${LIBUSB_SRC_DIR}/libusb" CACHE PATH "libusb headers path" FORCE)
# MINGW: causes "externals/libusb/libusb/libusb/os/windows_winusb.c:1427:2: error: conversion to non-scalar type requested", so cannot statically link it for now.
if (NOT MINGW)
set(LIBUSB_CFLAGS "-DGUID_DEVINTERFACE_USB_DEVICE=\\(GUID\\){0xA5DCBF10,0x6530,0x11D2,{0x90,0x1F,0x00,0xC0,0x4F,0xB9,0x51,0xED}}")
endif()
make_directory("${LIBUSB_PREFIX}") make_directory("${LIBUSB_PREFIX}")
add_custom_command( add_custom_command(
@ -146,8 +141,6 @@ else() # MINGW OR (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
target_include_directories(usb BEFORE PRIVATE libusb/msvc) target_include_directories(usb BEFORE PRIVATE libusb/msvc)
endif() endif()
# Works around other libraries providing their own definition of USB GUIDs (e.g. SDL2)
target_compile_definitions(usb PRIVATE "-DGUID_DEVINTERFACE_USB_DEVICE=(GUID){ 0xA5DCBF10, 0x6530, 0x11D2, {0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED}}")
else() else()
target_include_directories(usb target_include_directories(usb
# turns out other projects also have "config.h", so make sure the # turns out other projects also have "config.h", so make sure the

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -120,6 +120,8 @@ add_library(common STATIC
socket_types.h socket_types.h
spin_lock.cpp spin_lock.cpp
spin_lock.h spin_lock.h
stb.cpp
stb.h
steady_clock.cpp steady_clock.cpp
steady_clock.h steady_clock.h
stream.cpp stream.cpp
@ -208,6 +210,8 @@ if (MSVC)
/we4254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data /we4254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data
/we4800 # Implicit conversion from 'type' to bool. Possible information loss /we4800 # Implicit conversion from 'type' to bool. Possible information loss
) )
else()
set_source_files_properties(stb.cpp PROPERTIES COMPILE_OPTIONS "-Wno-implicit-fallthrough;-Wno-missing-declarations;-Wno-missing-field-initializers")
endif() endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
@ -223,7 +227,7 @@ endif()
create_target_directory_groups(common) create_target_directory_groups(common)
target_link_libraries(common PUBLIC Boost::context Boost::headers fmt::fmt microprofile Threads::Threads) target_link_libraries(common PUBLIC Boost::context Boost::headers fmt::fmt microprofile stb::headers Threads::Threads)
target_link_libraries(common PRIVATE lz4::lz4 zstd::zstd LLVM::Demangle) target_link_libraries(common PRIVATE lz4::lz4 zstd::zstd LLVM::Demangle)
if (ANDROID) if (ANDROID)

8
src/common/stb.cpp Executable file
View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "common/stb.h"

8
src/common/stb.h Executable file
View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stb_image.h>
#include <stb_image_resize.h>
#include <stb_image_write.h>

View file

@ -2,13 +2,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <sstream> #include <sstream>
#include <stb_image.h>
#include <stb_image_resize.h>
#include <stb_image_write.h>
#include "common/fs/file.h" #include "common/fs/file.h"
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/stb.h"
#include "core/core.h" #include "core/core.h"
#include "core/hle/service/caps/caps_manager.h" #include "core/hle/service/caps/caps_manager.h"
#include "core/hle/service/caps/caps_result.h" #include "core/hle/service/caps/caps_result.h"
@ -409,6 +407,12 @@ Result AlbumManager::LoadImage(std::span<u8> out_image, const std::filesystem::p
return ResultSuccess; return ResultSuccess;
} }
static void PNGToMemory(void* context, void* png, int len) {
std::vector<u8>* png_image = static_cast<std::vector<u8>*>(context);
png_image->reserve(len);
std::memcpy(png_image->data(), png, len);
}
Result AlbumManager::SaveImage(ApplicationAlbumEntry& out_entry, std::span<const u8> image, Result AlbumManager::SaveImage(ApplicationAlbumEntry& out_entry, std::span<const u8> image,
u64 title_id, const AlbumFileDateTime& date) const { u64 title_id, const AlbumFileDateTime& date) const {
const auto screenshot_path = const auto screenshot_path =
@ -422,16 +426,12 @@ Result AlbumManager::SaveImage(ApplicationAlbumEntry& out_entry, std::span<const
const Common::FS::IOFile db_file{file_path, Common::FS::FileAccessMode::Write, const Common::FS::IOFile db_file{file_path, Common::FS::FileAccessMode::Write,
Common::FS::FileType::BinaryFile}; Common::FS::FileType::BinaryFile};
s32 len; std::vector<u8> png_image;
const u8* png = stbi_write_png_to_mem(image.data(), 0, 1280, 720, STBI_rgb_alpha, &len); if (!stbi_write_png_to_func(PNGToMemory, &png_image, 1280, 720, STBI_rgb_alpha, image.data(),
0)) {
if (!png) {
return ResultFileCountLimit; return ResultFileCountLimit;
} }
std::vector<u8> png_image(len);
std::memcpy(png_image.data(), png, len);
if (db_file.Write(png_image) != png_image.size()) { if (db_file.Write(png_image) != png_image.size()) {
return ResultFileCountLimit; return ResultFileCountLimit;
} }

View file

@ -380,7 +380,6 @@ void GameList::UnloadController() {
GameList::~GameList() { GameList::~GameList() {
UnloadController(); UnloadController();
emit ShouldCancelWorker();
} }
void GameList::SetFilterFocus() { void GameList::SetFilterFocus() {
@ -397,6 +396,10 @@ void GameList::ClearFilter() {
search_field->clear(); search_field->clear();
} }
void GameList::WorkerEvent() {
current_worker->ProcessEvents(this);
}
void GameList::AddDirEntry(GameListDir* entry_items) { void GameList::AddDirEntry(GameListDir* entry_items) {
item_model->invisibleRootItem()->appendRow(entry_items); item_model->invisibleRootItem()->appendRow(entry_items);
tree_view->setExpanded( tree_view->setExpanded(
@ -826,28 +829,21 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size); tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size);
tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time); tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
// Before deleting rows, cancel the worker so that it is not using them // Cancel any existing worker.
emit ShouldCancelWorker(); current_worker.reset();
// Delete any rows that might already exist if we're repopulating // Delete any rows that might already exist if we're repopulating
item_model->removeRows(0, item_model->rowCount()); item_model->removeRows(0, item_model->rowCount());
search_field->clear(); search_field->clear();
GameListWorker* worker = current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, compatibility_list,
new GameListWorker(vfs, provider, game_dirs, compatibility_list, play_time_manager, system); play_time_manager, system);
connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); // Get events from the worker as data becomes available
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent,
Qt::QueuedConnection); Qt::QueuedConnection);
connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
Qt::QueuedConnection);
// Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
// cancel without delay.
connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
Qt::DirectConnection);
QThreadPool::globalInstance()->start(worker); QThreadPool::globalInstance()->start(current_worker.get());
current_worker = std::move(worker);
} }
void GameList::SaveInterfaceLayout() { void GameList::SaveInterfaceLayout() {

View file

@ -109,7 +109,6 @@ signals:
void BootGame(const QString& game_path, u64 program_id, std::size_t program_index, void BootGame(const QString& game_path, u64 program_id, std::size_t program_index,
StartGameType type, AmLaunchType launch_type); StartGameType type, AmLaunchType launch_type);
void GameChosen(const QString& game_path, const u64 title_id = 0); void GameChosen(const QString& game_path, const u64 title_id = 0);
void ShouldCancelWorker();
void OpenFolderRequested(u64 program_id, GameListOpenTarget target, void OpenFolderRequested(u64 program_id, GameListOpenTarget target,
const std::string& game_path); const std::string& game_path);
void OpenTransferableShaderCacheRequested(u64 program_id); void OpenTransferableShaderCacheRequested(u64 program_id);
@ -138,11 +137,16 @@ private slots:
void OnUpdateThemedIcons(); void OnUpdateThemedIcons();
private: private:
friend class GameListWorker;
void WorkerEvent();
void AddDirEntry(GameListDir* entry_items); void AddDirEntry(GameListDir* entry_items);
void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent); void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
void ValidateEntry(const QModelIndex& item);
void DonePopulating(const QStringList& watch_list); void DonePopulating(const QStringList& watch_list);
private:
void ValidateEntry(const QModelIndex& item);
void RefreshGameDirectory(); void RefreshGameDirectory();
void ToggleFavorite(u64 program_id); void ToggleFavorite(u64 program_id);
@ -165,7 +169,7 @@ private:
QVBoxLayout* layout = nullptr; QVBoxLayout* layout = nullptr;
QTreeView* tree_view = nullptr; QTreeView* tree_view = nullptr;
QStandardItemModel* item_model = nullptr; QStandardItemModel* item_model = nullptr;
GameListWorker* current_worker = nullptr; std::unique_ptr<GameListWorker> current_worker;
QFileSystemWatcher* watcher = nullptr; QFileSystemWatcher* watcher = nullptr;
ControllerNavigation* controller_navigation = nullptr; ControllerNavigation* controller_navigation = nullptr;
CompatibilityList compatibility_list; CompatibilityList compatibility_list;

View file

@ -233,10 +233,53 @@ GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_,
const PlayTime::PlayTimeManager& play_time_manager_, const PlayTime::PlayTimeManager& play_time_manager_,
Core::System& system_) Core::System& system_)
: vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_}, : vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_},
compatibility_list{compatibility_list_}, compatibility_list{compatibility_list_}, play_time_manager{play_time_manager_}, system{
play_time_manager{play_time_manager_}, system{system_} {} system_} {
// We want the game list to manage our lifetime.
setAutoDelete(false);
}
GameListWorker::~GameListWorker() = default; GameListWorker::~GameListWorker() {
this->disconnect();
stop_requested.store(true);
processing_completed.Wait();
}
void GameListWorker::ProcessEvents(GameList* game_list) {
while (true) {
std::function<void(GameList*)> func;
{
// Lock queue to protect concurrent modification.
std::scoped_lock lk(lock);
// If we can't pop a function, return.
if (queued_events.empty()) {
return;
}
// Pop a function.
func = std::move(queued_events.back());
queued_events.pop_back();
}
// Run the function.
func(game_list);
}
}
template <typename F>
void GameListWorker::RecordEvent(F&& func) {
{
// Lock queue to protect concurrent modification.
std::scoped_lock lk(lock);
// Add the function into the front of the queue.
queued_events.emplace_front(std::move(func));
}
// Data now available.
emit DataAvailable();
}
void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) { void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
using namespace FileSys; using namespace FileSys;
@ -284,9 +327,9 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
GetMetadataFromControlNCA(patch, *control, icon, name); GetMetadataFromControlNCA(patch, *control, icon, name);
} }
emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader, auto entry = MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
program_id, compatibility_list, play_time_manager, patch), program_id, compatibility_list, play_time_manager, patch);
parent_dir); RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
} }
} }
@ -360,11 +403,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
const FileSys::PatchManager patch{id, system.GetFileSystemController(), const FileSys::PatchManager patch{id, system.GetFileSystemController(),
system.GetContentProvider()}; system.GetContentProvider()};
emit EntryReady(MakeGameListEntry(physical_name, name, auto entry = MakeGameListEntry(
Common::FS::GetSize(physical_name), icon, physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
*loader, id, compatibility_list, id, compatibility_list, play_time_manager, patch);
play_time_manager, patch),
parent_dir); RecordEvent(
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
} }
} else { } else {
std::vector<u8> icon; std::vector<u8> icon;
@ -376,11 +420,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), const FileSys::PatchManager patch{program_id, system.GetFileSystemController(),
system.GetContentProvider()}; system.GetContentProvider()};
emit EntryReady(MakeGameListEntry(physical_name, name, auto entry = MakeGameListEntry(
Common::FS::GetSize(physical_name), icon, physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
*loader, program_id, compatibility_list, program_id, compatibility_list, play_time_manager, patch);
play_time_manager, patch),
parent_dir); RecordEvent(
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
} }
} }
} else if (is_dir) { } else if (is_dir) {
@ -399,25 +444,34 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
} }
void GameListWorker::run() { void GameListWorker::run() {
watch_list.clear();
provider->ClearAllEntries(); provider->ClearAllEntries();
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); });
};
for (UISettings::GameDir& game_dir : game_dirs) { for (UISettings::GameDir& game_dir : game_dirs) {
if (stop_requested) {
break;
}
if (game_dir.path == QStringLiteral("SDMC")) { if (game_dir.path == QStringLiteral("SDMC")) {
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir); auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
emit DirEntryReady(game_list_dir); DirEntryReady(game_list_dir);
AddTitlesToGameList(game_list_dir); AddTitlesToGameList(game_list_dir);
} else if (game_dir.path == QStringLiteral("UserNAND")) { } else if (game_dir.path == QStringLiteral("UserNAND")) {
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir); auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
emit DirEntryReady(game_list_dir); DirEntryReady(game_list_dir);
AddTitlesToGameList(game_list_dir); AddTitlesToGameList(game_list_dir);
} else if (game_dir.path == QStringLiteral("SysNAND")) { } else if (game_dir.path == QStringLiteral("SysNAND")) {
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir); auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
emit DirEntryReady(game_list_dir); DirEntryReady(game_list_dir);
AddTitlesToGameList(game_list_dir); AddTitlesToGameList(game_list_dir);
} else { } else {
watch_list.append(game_dir.path); watch_list.append(game_dir.path);
auto* const game_list_dir = new GameListDir(game_dir); auto* const game_list_dir = new GameListDir(game_dir);
emit DirEntryReady(game_list_dir); DirEntryReady(game_list_dir);
ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(),
game_dir.deep_scan, game_list_dir); game_dir.deep_scan, game_list_dir);
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(), ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(),
@ -425,12 +479,6 @@ void GameListWorker::run() {
} }
} }
emit Finished(watch_list); RecordEvent([=](GameList* game_list) { game_list->DonePopulating(watch_list); });
processing_completed.Set(); processing_completed.Set();
} }
void GameListWorker::Cancel() {
this->disconnect();
stop_requested.store(true);
processing_completed.Wait();
}

View file

@ -4,6 +4,7 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <deque>
#include <memory> #include <memory>
#include <string> #include <string>
@ -20,6 +21,7 @@ namespace Core {
class System; class System;
} }
class GameList;
class QStandardItem; class QStandardItem;
namespace FileSys { namespace FileSys {
@ -46,24 +48,22 @@ public:
/// Starts the processing of directory tree information. /// Starts the processing of directory tree information.
void run() override; void run() override;
/// Tells the worker that it should no longer continue processing. Thread-safe. public:
void Cancel(); /**
* Synchronously processes any events queued by the worker.
*
* AddDirEntry is called on the game list for every discovered directory.
* AddEntry is called on the game list for every discovered program.
* DonePopulating is called on the game list when processing completes.
*/
void ProcessEvents(GameList* game_list);
signals: signals:
/** void DataAvailable();
* The `EntryReady` signal is emitted once an entry has been prepared and is ready
* to be added to the game list.
* @param entry_items a list with `QStandardItem`s that make up the columns of the new
* entry.
*/
void DirEntryReady(GameListDir* entry_items);
void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir);
/** private:
* After the worker has traversed the game directory looking for entries, this signal is template <typename F>
* emitted with a list of folders that should be watched for changes as well. void RecordEvent(F&& func);
*/
void Finished(QStringList watch_list);
private: private:
void AddTitlesToGameList(GameListDir* parent_dir); void AddTitlesToGameList(GameListDir* parent_dir);
@ -84,8 +84,11 @@ private:
QStringList watch_list; QStringList watch_list;
Common::Event processing_completed; std::mutex lock;
std::condition_variable cv;
std::deque<std::function<void(GameList*)>> queued_events;
std::atomic_bool stop_requested = false; std::atomic_bool stop_requested = false;
Common::Event processing_completed;
Core::System& system; Core::System& system;
}; };