Cheevos: Linux support

This commit is contained in:
Connor McLaughlin 2021-02-28 19:01:16 +10:00
parent a032d191c8
commit 10c3506f1a
10 changed files with 694 additions and 6 deletions

View file

@ -65,6 +65,10 @@ if(ANDROID)
message("Building for Android, disabling Discord Presence support")
set(ENABLE_DISCORD_PRESENCE OFF)
endif()
if(ENABLE_CHEEVOS)
message("Building for Android. disabling RetroAchievements support")
set(ENABLE_CHEEVOS OFF)
endif()
if(USE_SDL2)
message("Building for Android, disabling SDL2 support")
set(USE_SDL2 OFF)
@ -133,6 +137,13 @@ if(USE_EVDEV)
message(STATUS "EVDev Support enabled")
find_package(LIBEVDEV REQUIRED)
endif()
if(ENABLE_CHEEVOS)
message(STATUS "RetroAchievements support enabled")
if(NOT WIN32)
find_package(CURL REQUIRED)
endif()
endif()
# Set _DEBUG macro for Debug builds.
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -D_DEBUG")

View file

@ -28,5 +28,5 @@ add_library(rcheevos
target_include_directories(rcheevos PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_include_directories(rcheevos INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_compile_definitions(rcheevos PRIVATE "RC_DISABLE_LUA=1")

View file

@ -72,6 +72,8 @@ add_library(common
string.h
string_util.cpp
string_util.h
thirdparty/thread_pool.cpp
thirdparty/thread_pool.h
timer.cpp
timer.h
timestamp.cpp

View file

@ -177,9 +177,10 @@ ALWAYS_INLINE_RELEASE static T InBack(T t)
}
template<typename T>
ALWAYS_INLINE_RELEASE static T OutBack(T t)
static T OutBack(T t)
{
return 1 + (--t) * t * (2.70158f * t + 1.70158f);
t -= 1;
return 1 + t * t * (2.70158f * t + 1.70158f);
}
template<typename T>

111
src/common/thirdparty/thread_pool.cpp vendored Normal file
View file

@ -0,0 +1,111 @@
// From https://raw.githubusercontent.com/cbraley/threadpool/master/src/thread_pool.cc
#include "thread_pool.h"
#include <cassert>
namespace cb {
// static
unsigned int ThreadPool::GetNumLogicalCores() {
// TODO(cbraley): Apparently this is broken in some older stdlib
// implementations?
const unsigned int dflt = std::thread::hardware_concurrency();
if (dflt == 0) {
// TODO(cbraley): Return some error code instead.
return 16;
} else {
return dflt;
}
}
ThreadPool::~ThreadPool() {
// TODO(cbraley): The current thread could help out to drain the work_ queue
// faster - for example, if there is work that hasn't yet been scheduled this
// thread could "pitch in" to help finish faster.
{
std::lock_guard<std::mutex> scoped_lock(mu_);
exit_ = true;
}
condvar_.notify_all(); // Tell *all* workers we are ready.
for (std::thread& thread : workers_) {
thread.join();
}
}
void ThreadPool::Wait() {
std::unique_lock<std::mutex> lock(mu_);
if (!work_.empty()) {
work_done_condvar_.wait(lock, [this] { return work_.empty(); });
}
}
ThreadPool::ThreadPool(int num_workers)
: num_workers_(num_workers), exit_(false) {
assert(num_workers_ > 0);
// TODO(cbraley): Handle thread construction exceptions.
workers_.reserve(num_workers_);
for (int i = 0; i < num_workers_; ++i) {
workers_.emplace_back(&ThreadPool::ThreadLoop, this);
}
}
void ThreadPool::Schedule(std::function<void(void)> func) {
ScheduleAndGetFuture(std::move(func)); // We ignore the returned std::future.
}
void ThreadPool::ThreadLoop() {
// Wait until the ThreadPool sends us work.
while (true) {
WorkItem work_item;
int prev_work_size = -1;
{
std::unique_lock<std::mutex> lock(mu_);
condvar_.wait(lock, [this] { return exit_ || (!work_.empty()); });
// ...after the wait(), we hold the lock.
// If all the work is done and exit_ is true, break out of the loop.
if (exit_ && work_.empty()) {
break;
}
// Pop the work off of the queue - we are careful to execute the
// work_item.func callback only after we have released the lock.
prev_work_size = work_.size();
work_item = std::move(work_.front());
work_.pop();
}
// We are careful to do the work without the lock held!
// TODO(cbraley): Handle exceptions properly.
work_item.func(); // Do work.
if (work_done_callback_) {
work_done_callback_(prev_work_size - 1);
}
// Notify a condvar is all work is done.
{
std::unique_lock<std::mutex> lock(mu_);
if (work_.empty() && prev_work_size == 1) {
work_done_condvar_.notify_all();
}
}
}
}
int ThreadPool::OutstandingWorkSize() const {
std::lock_guard<std::mutex> scoped_lock(mu_);
return work_.size();
}
int ThreadPool::NumWorkers() const { return num_workers_; }
void ThreadPool::SetWorkDoneCallback(std::function<void(int)> func) {
work_done_callback_ = std::move(func);
}
} // namespace cb

234
src/common/thirdparty/thread_pool.h vendored Normal file
View file

@ -0,0 +1,234 @@
// From https://raw.githubusercontent.com/cbraley/threadpool/master/src/thread_pool.h
#ifndef SRC_THREAD_POOL_H_
#define SRC_THREAD_POOL_H_
// A simple thread pool class.
// Usage examples:
//
// {
// ThreadPool pool(16); // 16 worker threads.
// for (int i = 0; i < 100; ++i) {
// pool.Schedule([i]() {
// DoSlowExpensiveOperation(i);
// });
// }
//
// // `pool` goes out of scope here - the code will block in the ~ThreadPool
// // destructor until all work is complete.
// }
//
// // TODO(cbraley): Add examples with std::future.
#include <condition_variable>
#include <functional>
#include <future>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
// We want to use std::invoke if C++17 is available, and fallback to "hand
// crafted" code if std::invoke isn't available.
#if __cplusplus >= 201703L
#define INVOKE_MACRO(CALLABLE, ARGS_TYPE, ARGS) std::invoke(CALLABLE, std::forward<ARGS_TYPE>(ARGS)...)
#elif __cplusplus >= 201103L
// Update this with http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4169.html.
#define INVOKE_MACRO(CALLABLE, ARGS_TYPE, ARGS) CALLABLE(std::forward<ARGS_TYPE>(ARGS)...)
#else
#error ("C++ version is too old! C++98 is not supported.")
#endif
namespace cb {
class ThreadPool {
public:
// Create a thread pool with `num_workers` dedicated worker threads.
explicit ThreadPool(int num_workers);
// Default construction is disallowed.
ThreadPool() = delete;
// Get the number of logical cores on the CPU. This is implemented using
// std::thread::hardware_concurrency().
// https://en.cppreference.com/w/cpp/thread/thread/hardware_concurrency
static unsigned int GetNumLogicalCores();
// The `ThreadPool` destructor blocks until all outstanding work is complete.
~ThreadPool();
// No copying, assigning, or std::move-ing.
ThreadPool& operator=(const ThreadPool&) = delete;
ThreadPool(const ThreadPool&) = delete;
ThreadPool(ThreadPool&&) = delete;
ThreadPool& operator=(ThreadPool&&) = delete;
// Add the function `func` to the thread pool. `func` will be executed at some
// point in the future on an arbitrary thread.
void Schedule(std::function<void(void)> func);
// Add `func` to the thread pool, and return a std::future that can be used to
// access the function's return value.
//
// *** Usage example ***
// Don't be alarmed by this function's tricky looking signature - this is
// very easy to use. Here's an example:
//
// int ComputeSum(std::vector<int>& values) {
// int sum = 0;
// for (const int& v : values) {
// sum += v;
// }
// return sum;
// }
//
// ThreadPool pool = ...;
// std::vector<int> numbers = ...;
//
// std::future<int> sum_future = ScheduleAndGetFuture(
// []() {
// return ComputeSum(numbers);
// });
//
// // Do other work...
//
// std::cout << "The sum is " << sum_future.get() << std::endl;
//
// *** Details ***
// Given a callable `func` that returns a value of type `RetT`, this
// function returns a std::future<RetT> that can be used to access
// `func`'s results.
template <typename FuncT, typename... ArgsT>
auto ScheduleAndGetFuture(FuncT&& func, ArgsT&&... args)
-> std::future<decltype(INVOKE_MACRO(func, ArgsT, args))>;
// Wait for all outstanding work to be completed.
void Wait();
// Return the number of outstanding functions to be executed.
int OutstandingWorkSize() const;
// Return the number of threads in the pool.
int NumWorkers() const;
void SetWorkDoneCallback(std::function<void(int)> func);
private:
void ThreadLoop();
// Number of worker threads - fixed at construction time.
int num_workers_;
// The destructor sets `exit_` to true and then notifies all workers. `exit_`
// causes each thread to break out of their work loop.
bool exit_;
mutable std::mutex mu_;
// Work queue. Guarded by `mu_`.
struct WorkItem {
std::function<void(void)> func;
};
std::queue<WorkItem> work_;
// Condition variable used to notify worker threads that new work is
// available.
std::condition_variable condvar_;
// Worker threads.
std::vector<std::thread> workers_;
// Condition variable used to notify that all work is complete - the work
// queue has "run dry".
std::condition_variable work_done_condvar_;
// Whenever a work item is complete, we call this callback. If this is empty,
// nothing is done.
std::function<void(int)> work_done_callback_;
};
namespace impl {
// This helper class simply returns a std::function that executes:
// ReturnT x = func();
// promise->set_value(x);
// However, this is tricky in the case where T == void. The code above won't
// compile if ReturnT == void, and neither will
// promise->set_value(func());
// To workaround this, we use a template specialization for the case where
// ReturnT is void. If the "regular void" proposal is accepted, this could be
// simpler:
// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0146r1.html.
// The non-specialized `FuncWrapper` implementation handles callables that
// return a non-void value.
template <typename ReturnT>
struct FuncWrapper {
template <typename FuncT, typename... ArgsT>
std::function<void()> GetWrapped(
FuncT&& func, std::shared_ptr<std::promise<ReturnT>> promise,
ArgsT&&... args) {
// TODO(cbraley): Capturing by value is inefficient. It would be more
// efficient to move-capture everything, but we can't do this until C++14
// generalized lambda capture is available. Can we use std::bind instead to
// make this more efficient and still use C++11?
return [promise, func, args...]() mutable {
promise->set_value(INVOKE_MACRO(func, ArgsT, args));
};
}
};
template <typename FuncT, typename... ArgsT>
void InvokeVoidRet(FuncT&& func, std::shared_ptr<std::promise<void>> promise,
ArgsT&&... args) {
INVOKE_MACRO(func, ArgsT, args);
promise->set_value();
}
// This `FuncWrapper` specialization handles callables that return void.
template <>
struct FuncWrapper<void> {
template <typename FuncT, typename... ArgsT>
std::function<void()> GetWrapped(FuncT&& func,
std::shared_ptr<std::promise<void>> promise,
ArgsT&&... args) {
return [promise, func, args...]() mutable {
INVOKE_MACRO(func, ArgsT, args);
promise->set_value();
};
}
};
} // namespace impl
template <typename FuncT, typename... ArgsT>
auto ThreadPool::ScheduleAndGetFuture(FuncT&& func, ArgsT&&... args)
-> std::future<decltype(INVOKE_MACRO(func, ArgsT, args))> {
using ReturnT = decltype(INVOKE_MACRO(func, ArgsT, args));
// We are only allocating this std::promise in a shared_ptr because
// std::promise is non-copyable.
std::shared_ptr<std::promise<ReturnT>> promise =
std::make_shared<std::promise<ReturnT>>();
std::future<ReturnT> ret_future = promise->get_future();
impl::FuncWrapper<ReturnT> func_wrapper;
std::function<void()> wrapped_func = func_wrapper.GetWrapped(
std::move(func), std::move(promise), std::forward<ArgsT>(args)...);
// Acquire the lock, and then push the WorkItem onto the queue.
{
std::lock_guard<std::mutex> scoped_lock(mu_);
WorkItem work;
work.func = std::move(wrapped_func);
work_.emplace(std::move(work));
}
condvar_.notify_one(); // Tell one worker we are ready.
return ret_future;
}
} // namespace cb
#undef INVOKE_MACRO
#endif // SRC_THREAD_POOL_H_

View file

@ -103,7 +103,7 @@ set(SRCS
settingsdialog.ui
)
if(WITH_CHEEVOS)
if(ENABLE_CHEEVOS)
set(SRCS ${SRCS}
achievementlogindialog.cpp
achievementlogindialog.h

View file

@ -76,7 +76,6 @@ if(SDL2_FOUND)
endif()
if(USE_EVDEV)
find_package(LIBEVDEV REQUIRED)
target_compile_definitions(frontend-common PUBLIC "-DWITH_EVDEV=1")
target_include_directories(frontend-common PRIVATE ${LIBEVDEV_INCLUDE_DIRS})
target_link_libraries(frontend-common PRIVATE ${LIBEVDEV_LIBRARIES})
@ -101,6 +100,14 @@ if(ENABLE_CHEEVOS)
http_downloader_winhttp.cpp
http_downloader_winhttp.h
)
else()
target_sources(frontend-common PRIVATE
http_downloader_curl.cpp
http_downloader_curl.h
)
target_link_libraries(frontend-common PRIVATE
CURL::libcurl
)
endif()
target_sources(frontend-common PRIVATE

View file

@ -0,0 +1,283 @@
#include "http_downloader_curl.h"
#include "common/assert.h"
#include "common/log.h"
#include "common/string_util.h"
#include "common/timer.h"
#include <algorithm>
#include <functional>
Log_SetChannel(HTTPDownloaderCurl);
namespace FrontendCommon {
HTTPDownloaderCurl::HTTPDownloaderCurl() : HTTPDownloader() {}
HTTPDownloaderCurl::~HTTPDownloaderCurl() = default;
std::unique_ptr<HTTPDownloader> HTTPDownloader::Create()
{
std::unique_ptr<HTTPDownloaderCurl> instance(std::make_unique<HTTPDownloaderCurl>());
if (!instance->Initialize())
return {};
return instance;
}
static bool s_curl_initialized = false;
static std::once_flag s_curl_initialized_once_flag;
bool HTTPDownloaderCurl::Initialize()
{
if (!s_curl_initialized)
{
std::call_once(s_curl_initialized_once_flag, []() {
s_curl_initialized = curl_global_init(CURL_GLOBAL_ALL) == CURLE_OK;
if (s_curl_initialized)
{
std::atexit([]() {
curl_global_cleanup();
s_curl_initialized = false;
});
}
});
if (!s_curl_initialized)
{
Log_ErrorPrint("curl_global_init() failed");
return false;
}
}
m_thread_pool = std::make_unique<cb::ThreadPool>(m_max_active_requests);
return true;
}
/*
void CALLBACK HTTPDownloaderCurl::HTTPStatusCallback(HINTERNET hRequest, DWORD_PTR dwContext, DWORD dwInternetStatus,
LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
Request* req = reinterpret_cast<Request*>(dwContext);
switch (dwInternetStatus)
{
case WINHTTP_CALLBACK_STATUS_HANDLE_CREATED:
return;
case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING:
{
if (!req)
return;
DebugAssert(hRequest == req->hRequest);
HTTPDownloaderCurl* parent = static_cast<HTTPDownloaderCurl*>(req->parent);
std::unique_lock<std::mutex> lock(parent->m_pending_http_request_lock);
Assert(std::none_of(parent->m_pending_http_requests.begin(), parent->m_pending_http_requests.end(),
[req](HTTPDownloader::Request* it) { return it == req; }));
// we can clean up the connection as well
DebugAssert(req->hConnection != NULL);
WinHttpCloseHandle(req->hConnection);
delete req;
return;
}
case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
{
const WINHTTP_ASYNC_RESULT* res = reinterpret_cast<const WINHTTP_ASYNC_RESULT*>(lpvStatusInformation);
Log_ErrorPrintf("WinHttp async function %p returned error %u", res->dwResult, res->dwError);
req->status_code = -1;
req->state.store(Request::State::Complete);
return;
}
case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
{
Log_DevPrintf("SendRequest complete");
if (!WinHttpReceiveResponse(hRequest, nullptr))
{
Log_ErrorPrintf("WinHttpReceiveResponse() failed: %u", GetLastError());
req->status_code = -1;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
{
Log_DevPrintf("Headers available");
DWORD buffer_size = sizeof(req->status_code);
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &req->status_code, &buffer_size, WINHTTP_NO_HEADER_INDEX))
{
Log_ErrorPrintf("WinHttpQueryHeaders() for status code failed: %u", GetLastError());
req->status_code = -1;
req->state.store(Request::State::Complete);
return;
}
buffer_size = sizeof(req->content_length);
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &req->content_length, &buffer_size,
WINHTTP_NO_HEADER_INDEX))
{
if (GetLastError() != ERROR_WINHTTP_HEADER_NOT_FOUND)
Log_WarningPrintf("WinHttpQueryHeaders() for content length failed: %u", GetLastError());
req->content_length = 0;
}
Log_DevPrintf("Status code %d, content-length is %u", req->status_code, req->content_length);
req->data.reserve(req->content_length);
req->state = Request::State::Receiving;
// start reading
if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING)
{
Log_ErrorPrintf("WinHttpQueryDataAvailable() failed: %u", GetLastError());
req->status_code = -1;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE:
{
DWORD bytes_available;
std::memcpy(&bytes_available, lpvStatusInformation, sizeof(bytes_available));
if (bytes_available == 0)
{
// end of request
Log_DevPrintf("End of request '%s', %zu bytes received", req->url.c_str(), req->data.size());
req->state.store(Request::State::Complete);
return;
}
// start the transfer
Log_DevPrintf("%u bytes available", bytes_available);
req->io_position = static_cast<u32>(req->data.size());
req->data.resize(req->io_position + bytes_available);
if (!WinHttpReadData(hRequest, req->data.data() + req->io_position, bytes_available, nullptr) &&
GetLastError() != ERROR_IO_PENDING)
{
Log_ErrorPrintf("WinHttpReadData() failed: %u", GetLastError());
req->status_code = -1;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
{
Log_DevPrintf("Read of %u complete", dwStatusInformationLength);
const u32 new_size = req->io_position + dwStatusInformationLength;
Assert(new_size <= req->data.size());
req->data.resize(new_size);
req->start_time = Common::Timer::GetValue();
if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING)
{
Log_ErrorPrintf("WinHttpQueryDataAvailable() failed: %u", GetLastError());
req->status_code = -1;
req->state.store(Request::State::Complete);
}
return;
}
default:
// unhandled, ignore
return;
}
}
*/
size_t HTTPDownloaderCurl::WriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata)
{
Request* req = static_cast<Request*>(userdata);
const size_t current_size = req->data.size();
const size_t transfer_size = size * nmemb;
const size_t new_size = current_size + transfer_size;
req->data.resize(new_size);
std::memcpy(&req->data[current_size], ptr, transfer_size);
return nmemb;
}
void HTTPDownloaderCurl::ProcessRequest(Request* req)
{
std::unique_lock<std::mutex> cancel_lock(m_cancel_mutex);
if (req->closed.load())
return;
cancel_lock.unlock();
req->start_time = Common::Timer::GetValue();
int ret = curl_easy_perform(req->handle);
if (ret == CURLE_OK)
{
long response_code = 0;
curl_easy_getinfo(req->handle, CURLINFO_RESPONSE_CODE, &response_code);
req->status_code = static_cast<s32>(response_code);
Log_DevPrintf("Request for '%s' returned status code %d and %zu bytes",
req->url.c_str(), req->status_code, req->data.size());
}
else
{
Log_ErrorPrintf("Request for '%s' returned %d", req->url.c_str(), ret);
}
curl_easy_cleanup(req->handle);
cancel_lock.lock();
req->state = Request::State::Complete;
if (req->closed.load())
delete req;
else
req->closed.store(true);
}
HTTPDownloader::Request* HTTPDownloaderCurl::InternalCreateRequest()
{
Request* req = new Request();
req->handle = curl_easy_init();
if (!req->handle)
{
delete req;
return nullptr;
}
return req;
}
void HTTPDownloaderCurl::InternalPollRequests()
{
// noop - uses thread pool
}
bool HTTPDownloaderCurl::StartRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
curl_easy_setopt(req->handle, CURLOPT_URL, request->url.c_str());
// curl_easy_setopt(req->handle, CURLOPT_USERAGENT, m_user_agent.c_str());
curl_easy_setopt(req->handle, CURLOPT_WRITEFUNCTION, &HTTPDownloaderCurl::WriteCallback);
curl_easy_setopt(req->handle, CURLOPT_WRITEDATA, req);
if (request->type == Request::Type::Post)
{
curl_easy_setopt(req->handle, CURLOPT_POST, 1L);
curl_easy_setopt(req->handle, CURLOPT_POSTFIELDS, request->post_data.c_str());
}
Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
req->start_time = Common::Timer::GetValue();
m_thread_pool->Schedule(std::bind(&HTTPDownloaderCurl::ProcessRequest, this, req));
return true;
}
void HTTPDownloaderCurl::CloseRequest(HTTPDownloader::Request* request)
{
std::unique_lock<std::mutex> cancel_lock(m_cancel_mutex);
Request* req = static_cast<Request*>(request);
if (req->closed.load())
delete req;
else
req->closed.store(true);
}
} // namespace FrontendCommon

View file

@ -0,0 +1,39 @@
#pragma once
#include "http_downloader.h"
#include "common/thirdparty/thread_pool.h"
#include <atomic>
#include <memory>
#include <mutex>
#include <curl/curl.h>
namespace FrontendCommon {
class HTTPDownloaderCurl final : public HTTPDownloader
{
public:
HTTPDownloaderCurl();
~HTTPDownloaderCurl() override;
bool Initialize();
protected:
Request* InternalCreateRequest() override;
void InternalPollRequests() override;
bool StartRequest(HTTPDownloader::Request* request) override;
void CloseRequest(HTTPDownloader::Request* request) override;
private:
struct Request : HTTPDownloader::Request
{
CURL* handle = nullptr;
std::atomic_bool closed{false};
};
static size_t WriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata);
void ProcessRequest(Request* req);
std::unique_ptr<cb::ThreadPool> m_thread_pool;
std::mutex m_cancel_mutex;
};
} // namespace FrontendCommon