Merge remote-tracking branch 'upstream/master'

This commit is contained in:
987123879113 2022-09-18 01:58:40 +09:00
commit 4da56f58e7
61 changed files with 4261 additions and 909 deletions

View File

@ -118,6 +118,41 @@ declare -a SYSLIBS=(
"libpng16.so.16"
"libudev.so.1"
"libuuid.so.1"
"libcurl-gnutls.so.4"
"libnghttp2.so.14"
"libidn2.so.0"
"librtmp.so.1"
"libssh.so.4"
"libpsl.so.5"
"libnettle.so.7"
"libgnutls.so.30"
"libgssapi_krb5.so.2"
"libldap_r-2.4.so.2"
"liblber-2.4.so.2"
"libbrotlidec.so.1"
"libunistring.so.2"
"libhogweed.so.5"
"libgmp.so.10"
"libp11-kit.so.0"
"libtasn1.so.6"
"libkrb5.so.3"
"libk5crypto.so.3"
"libcom_err.so.2"
"libkrb5support.so.0"
"libsasl2.so.2"
"libgssapi.so.3"
"libbrotlicommon.so.1"
"libkeyutils.so.1"
"libheimntlm.so.0"
"libkrb5.so.26"
"libasn1.so.8"
"libhcrypto.so.4"
"libroken.so.18"
"libwind.so.0"
"libheimbase.so.1"
"libhx509.so.5"
"libsqlite3.so.0"
"libcrypt.so.1"
)
declare -a DEPLIBS=(

View File

@ -36,6 +36,7 @@ declare -a BUILD_PACKAGES=(
declare -a PCSX2_PACKAGES=(
"libaio-dev"
"libbz2-dev"
"libcurl4-gnutls-dev"
"libegl1-mesa-dev"
"libgl1-mesa-dev"
"libgtk-3-dev"

View File

@ -1086,29 +1086,29 @@ ImGuiStyle::ImGuiStyle()
// Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times.
void ImGuiStyle::ScaleAllSizes(float scale_factor)
{
WindowPadding = ImFloor(WindowPadding * scale_factor);
WindowRounding = ImFloor(WindowRounding * scale_factor);
WindowMinSize = ImFloor(WindowMinSize * scale_factor);
ChildRounding = ImFloor(ChildRounding * scale_factor);
PopupRounding = ImFloor(PopupRounding * scale_factor);
FramePadding = ImFloor(FramePadding * scale_factor);
FrameRounding = ImFloor(FrameRounding * scale_factor);
ItemSpacing = ImFloor(ItemSpacing * scale_factor);
ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor);
CellPadding = ImFloor(CellPadding * scale_factor);
TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor);
IndentSpacing = ImFloor(IndentSpacing * scale_factor);
ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor);
ScrollbarSize = ImFloor(ScrollbarSize * scale_factor);
ScrollbarRounding = ImFloor(ScrollbarRounding * scale_factor);
GrabMinSize = ImFloor(GrabMinSize * scale_factor);
GrabRounding = ImFloor(GrabRounding * scale_factor);
LogSliderDeadzone = ImFloor(LogSliderDeadzone * scale_factor);
TabRounding = ImFloor(TabRounding * scale_factor);
TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImFloor(TabMinWidthForCloseButton * scale_factor) : FLT_MAX;
DisplayWindowPadding = ImFloor(DisplayWindowPadding * scale_factor);
DisplaySafeAreaPadding = ImFloor(DisplaySafeAreaPadding * scale_factor);
MouseCursorScale = ImFloor(MouseCursorScale * scale_factor);
WindowPadding = ImFloor(WindowPadding * scale_factor + ImVec2(0.5f, 0.5f));
WindowRounding = ImFloor(WindowRounding * scale_factor + 0.5f);
WindowMinSize = ImFloor(WindowMinSize * scale_factor + ImVec2(0.5f, 0.5f));
ChildRounding = ImFloor(ChildRounding * scale_factor + 0.5f);
PopupRounding = ImFloor(PopupRounding * scale_factor + 0.5f);
FramePadding = ImFloor(FramePadding * scale_factor + ImVec2(0.5f, 0.5f));
FrameRounding = ImFloor(FrameRounding * scale_factor + 0.5f);
ItemSpacing = ImFloor(ItemSpacing * scale_factor + ImVec2(0.5f, 0.5f));
ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor + ImVec2(0.5f, 0.5f));
CellPadding = ImFloor(CellPadding * scale_factor + ImVec2(0.5f, 0.5f));
TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor + ImVec2(0.5f, 0.5f));
IndentSpacing = ImFloor(IndentSpacing * scale_factor + 0.5f);
ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor + 0.5f);
ScrollbarSize = ImFloor(ScrollbarSize * scale_factor + 0.5f);
ScrollbarRounding = ImFloor(ScrollbarRounding * scale_factor + 0.5f);
GrabMinSize = ImFloor(GrabMinSize * scale_factor + 0.5f);
GrabRounding = ImFloor(GrabRounding * scale_factor + 0.5f);
LogSliderDeadzone = ImFloor(LogSliderDeadzone * scale_factor + 0.5f);
TabRounding = ImFloor(TabRounding * scale_factor + 0.5f);
TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImFloor(TabMinWidthForCloseButton * scale_factor + 0.5f) : FLT_MAX;
DisplayWindowPadding = ImFloor(DisplayWindowPadding * scale_factor + ImVec2(0.5f, 0.5f));
DisplaySafeAreaPadding = ImFloor(DisplaySafeAreaPadding * scale_factor + ImVec2(0.5f, 0.5f));
MouseCursorScale = ImFloor(MouseCursorScale * scale_factor + 0.5f);
}
ImGuiIO::ImGuiIO()

View File

@ -4023,7 +4023,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
ImGuiInputTextState* state = GetInputTextState(id);
const bool input_requested_by_tabbing = (item_status_flags & ImGuiItemStatusFlags_FocusedByTabbing) != 0;
const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateInputId == id) || (g.NavActivateId == id && g.NavInputSource == ImGuiInputSource_Keyboard));
const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateInputId == id) || (g.NavActivateId == id && (g.NavInputSource == ImGuiInputSource_Keyboard || g.NavInputSource == ImGuiInputSource_Gamepad)));
const bool user_clicked = hovered && io.MouseClicked[0];
const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
@ -4299,7 +4299,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
// We allow validate/cancel with Nav source (gamepad) to makes it easier to undo an accidental NavInput press with no keyboard wired, but otherwise it isn't very useful.
const bool is_validate_enter = IsKeyPressed(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_KeypadEnter);
const bool is_validate_nav = (IsNavInputTest(ImGuiNavInput_Activate, ImGuiNavReadMode_Pressed) && !IsKeyPressed(ImGuiKey_Space)) || IsNavInputTest(ImGuiNavInput_Input, ImGuiNavReadMode_Pressed);
const bool is_cancel = IsKeyPressed(ImGuiKey_Escape) || IsNavInputTest(ImGuiNavInput_Cancel, ImGuiNavReadMode_Pressed);
const bool is_cancel = false; /*IsKeyPressed(ImGuiKey_Escape) || IsNavInputTest(ImGuiNavInput_Cancel, ImGuiNavReadMode_Pressed);*/
if (IsKeyPressed(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); }
else if (IsKeyPressed(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); }

View File

@ -237,6 +237,11 @@ if(QT_BUILD)
endif()
add_subdirectory(3rdparty/des)
if(NOT WIN32 AND QT_BUILD)
find_package(CURL REQUIRED)
endif()
add_subdirectory(3rdparty/lzma EXCLUDE_FROM_ALL)
add_subdirectory(3rdparty/libchdr EXCLUDE_FROM_ALL)

View File

@ -20,6 +20,7 @@ target_sources(common PRIVATE
FastJmp.cpp
FileSystem.cpp
Image.cpp
HTTPDownloader.cpp
Misc.cpp
MD5Digest.cpp
PrecompiledHeader.cpp
@ -29,6 +30,7 @@ target_sources(common PRIVATE
SettingsWrapper.cpp
StringUtil.cpp
Timer.cpp
ThreadPool.cpp
WindowInfo.cpp
emitter/bmi.cpp
emitter/cpudetect.cpp
@ -71,6 +73,7 @@ target_sources(common PRIVATE
HashCombine.h
Image.h
LRUCache.h
HTTPDownloader.h
MemcpyFast.h
MemsetFast.inl
MD5Digest.h
@ -87,6 +90,7 @@ target_sources(common PRIVATE
StringUtil.h
Timer.h
Threading.h
ThreadPool.h
TraceLog.h
WindowInfo.h
emitter/cpudetect_internal.h
@ -172,6 +176,8 @@ if(WIN32)
CrashHandler.cpp
CrashHandler.h
FastJmp.asm
HTTPDownloaderWinHTTP.cpp
HTTPDownloaderWinHTTP.h
StackWalker.cpp
StackWalker.h
D3D11/ShaderCache.cpp
@ -265,6 +271,15 @@ if (USE_GCC AND CMAKE_INTERPROCEDURAL_OPTIMIZATION)
set_source_files_properties(FastJmp.cpp PROPERTIES COMPILE_FLAGS -fno-lto)
endif()
if(NOT WIN32 AND (QT_BUILD OR NOGUI_BUILD))
# libcurl-based HTTPDownloader
target_sources(common PRIVATE
HTTPDownloaderCurl.cpp
HTTPDownloaderCurl.h
)
target_link_libraries(common PRIVATE CURL::libcurl)
endif()
target_link_libraries(common PRIVATE
${LIBC_LIBRARIES}
PNG::PNG

View File

@ -61,16 +61,32 @@ static std::time_t ConvertFileTimeToUnixTime(const FILETIME& ft)
}
#endif
static inline bool FileSystemCharacterIsSane(char c, bool StripSlashes)
static inline bool FileSystemCharacterIsSane(char c, bool strip_slashes)
{
if (!(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9') && c != ' ' &&
c != '_' && c != '-' && c != '.')
{
if (!StripSlashes && (c == '/' || c == '\\'))
return true;
#ifdef _WIN32
// https://docs.microsoft.com/en-gb/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#naming-conventions
if ((c == U'/' || c == U'\\') && strip_slashes)
return false;
if (c == U'<' || c == U'>' || c == U':' || c == U'"' || c == U'|' || c == U'?' || c == U'*' || c == 0 ||
c <= static_cast<char32_t>(31))
{
return false;
}
#else
if (c == '/' && strip_slashes)
return false;
// drop asterisks too, they make globbing annoying
if (c == '*')
return false;
// macos doesn't allow colons, apparently
#ifdef __APPLE__
if (c == U':')
return false;
#endif
#endif
return true;
}
@ -118,39 +134,58 @@ static inline void PathAppendString(std::string& dst, const T& src)
}
}
void Path::SanitizeFileName(char* Destination, u32 cbDestination, const char* FileName, bool StripSlashes /* = true */)
std::string Path::SanitizeFileName(const std::string_view& str, bool strip_slashes /* = true */)
{
u32 i;
u32 fileNameLength = static_cast<u32>(std::strlen(FileName));
std::string ret;
ret.reserve(str.length());
if (FileName == Destination)
size_t pos = 0;
while (pos < str.length())
{
for (i = 0; i < fileNameLength; i++)
{
if (!FileSystemCharacterIsSane(FileName[i], StripSlashes))
Destination[i] = '_';
}
}
else
{
for (i = 0; i < fileNameLength && i < cbDestination; i++)
{
if (FileSystemCharacterIsSane(FileName[i], StripSlashes))
Destination[i] = FileName[i];
else
Destination[i] = '_';
}
char32_t ch;
pos += StringUtil::DecodeUTF8(str, pos, &ch);
ch = FileSystemCharacterIsSane(ch, strip_slashes) ? ch : U'_';
StringUtil::EncodeAndAppendUTF8(ret, ch);
}
#ifdef _WIN32
// Windows: Can't end filename with a period.
if (ret.length() > 0 && ret.back() == '.')
ret.back() = '_';
#endif
return ret;
}
void Path::SanitizeFileName(std::string& Destination, bool StripSlashes /* = true*/)
void Path::SanitizeFileName(std::string* str, bool strip_slashes /* = true */)
{
const std::size_t len = Destination.length();
for (std::size_t i = 0; i < len; i++)
const size_t len = str->length();
char small_buf[128];
std::unique_ptr<char[]> large_buf;
char* str_copy = small_buf;
if (len >= std::size(small_buf))
{
if (!FileSystemCharacterIsSane(Destination[i], StripSlashes))
Destination[i] = '_';
large_buf = std::make_unique<char[]>(len + 1);
str_copy = large_buf.get();
}
std::memcpy(str_copy, str->c_str(), sizeof(char) * (len + 1));
str->clear();
size_t pos = 0;
while (pos < len)
{
char32_t ch;
pos += StringUtil::DecodeUTF8(str_copy + pos, pos - len, &ch);
ch = FileSystemCharacterIsSane(ch, strip_slashes) ? ch : U'_';
StringUtil::EncodeAndAppendUTF8(*str, ch);
}
#ifdef _WIN32
// Windows: Can't end filename with a period.
if (str->length() > 0 && str->back() == '.')
str->back() = '_';
#endif
}
bool Path::IsAbsolute(const std::string_view& path)

364
common/HTTPDownloader.cpp Normal file
View File

@ -0,0 +1,364 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "common/PrecompiledHeader.h"
#include "common/HTTPDownloader.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
using namespace Common;
static constexpr float DEFAULT_TIMEOUT_IN_SECONDS = 30;
static constexpr u32 DEFAULT_MAX_ACTIVE_REQUESTS = 4;
const char HTTPDownloader::DEFAULT_USER_AGENT[] =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0";
HTTPDownloader::HTTPDownloader()
: m_timeout(DEFAULT_TIMEOUT_IN_SECONDS)
, m_max_active_requests(DEFAULT_MAX_ACTIVE_REQUESTS)
{
}
HTTPDownloader::~HTTPDownloader() = default;
void HTTPDownloader::SetTimeout(float timeout)
{
m_timeout = timeout;
}
void HTTPDownloader::SetMaxActiveRequests(u32 max_active_requests)
{
pxAssert(max_active_requests > 0);
m_max_active_requests = max_active_requests;
}
void HTTPDownloader::CreateRequest(std::string url, Request::Callback callback)
{
Request* req = InternalCreateRequest();
req->parent = this;
req->type = Request::Type::Get;
req->url = std::move(url);
req->callback = std::move(callback);
req->start_time = Timer::GetCurrentValue();
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
if (LockedGetActiveRequestCount() < m_max_active_requests)
{
if (!StartRequest(req))
return;
}
LockedAddRequest(req);
}
void HTTPDownloader::CreatePostRequest(std::string url, std::string post_data, Request::Callback callback)
{
Request* req = InternalCreateRequest();
req->parent = this;
req->type = Request::Type::Post;
req->url = std::move(url);
req->post_data = std::move(post_data);
req->callback = std::move(callback);
req->start_time = Timer::GetCurrentValue();
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
if (LockedGetActiveRequestCount() < m_max_active_requests)
{
if (!StartRequest(req))
return;
}
LockedAddRequest(req);
}
void HTTPDownloader::LockedPollRequests(std::unique_lock<std::mutex>& lock)
{
if (m_pending_http_requests.empty())
return;
InternalPollRequests();
const Common::Timer::Value current_time = Timer::GetCurrentValue();
u32 active_requests = 0;
u32 unstarted_requests = 0;
for (size_t index = 0; index < m_pending_http_requests.size();)
{
Request* req = m_pending_http_requests[index];
if (req->state == Request::State::Pending)
{
unstarted_requests++;
index++;
continue;
}
if (req->state == Request::State::Started && current_time >= req->start_time &&
Common::Timer::ConvertValueToSeconds(current_time - req->start_time) >= m_timeout)
{
// request timed out
Console.Error("Request for '%s' timed out", req->url.c_str());
req->state.store(Request::State::Cancelled);
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
lock.unlock();
req->callback(-1, std::string(), Request::Data());
CloseRequest(req);
lock.lock();
continue;
}
if (req->state != Request::State::Complete)
{
active_requests++;
index++;
continue;
}
// request complete
DevCon.WriteLn("Request for '%s' complete, returned status code %u and %zu bytes", req->url.c_str(),
req->status_code, req->data.size());
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
// run callback with lock unheld
lock.unlock();
req->callback(req->status_code, std::move(req->content_type), std::move(req->data));
CloseRequest(req);
lock.lock();
}
// start new requests when we finished some
if (unstarted_requests > 0 && active_requests < m_max_active_requests)
{
for (size_t index = 0; index < m_pending_http_requests.size();)
{
Request* req = m_pending_http_requests[index];
if (req->state != Request::State::Pending)
{
index++;
continue;
}
if (!StartRequest(req))
{
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
continue;
}
active_requests++;
index++;
if (active_requests >= m_max_active_requests)
break;
}
}
}
void HTTPDownloader::PollRequests()
{
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
LockedPollRequests(lock);
}
void HTTPDownloader::WaitForAllRequests()
{
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
while (!m_pending_http_requests.empty())
LockedPollRequests(lock);
}
void HTTPDownloader::LockedAddRequest(Request* request)
{
m_pending_http_requests.push_back(request);
}
u32 HTTPDownloader::LockedGetActiveRequestCount()
{
u32 count = 0;
for (Request* req : m_pending_http_requests)
{
if (req->state == Request::State::Started || req->state == Request::State::Receiving)
count++;
}
return count;
}
bool HTTPDownloader::HasAnyRequests()
{
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
return !m_pending_http_requests.empty();
}
std::string HTTPDownloader::URLEncode(const std::string_view& str)
{
std::string ret;
ret.reserve(str.length() + ((str.length() + 3) / 4) * 3);
for (size_t i = 0, l = str.size(); i < l; i++)
{
const char c = str[i];
if ((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c == '-' || c == '_' || c == '.' || c == '!' || c == '~' ||
c == '*' || c == '\'' || c == '(' || c == ')')
{
ret.push_back(c);
}
else
{
ret.push_back('%');
const unsigned char n1 = static_cast<unsigned char>(c) >> 4;
const unsigned char n2 = static_cast<unsigned char>(c) & 0x0F;
ret.push_back((n1 >= 10) ? ('a' + (n1 - 10)) : ('0' + n1));
ret.push_back((n2 >= 10) ? ('a' + (n2 - 10)) : ('0' + n2));
}
}
return ret;
}
std::string HTTPDownloader::URLDecode(const std::string_view& str)
{
std::string ret;
ret.reserve(str.length());
for (size_t i = 0, l = str.size(); i < l; i++)
{
const char c = str[i];
if (c == '+')
{
ret.push_back(c);
}
else if (c == '%')
{
if ((i + 2) >= str.length())
break;
const char clower = str[i + 1];
const char cupper = str[i + 2];
const unsigned char lower = (clower >= '0' && clower <= '9') ? static_cast<unsigned char>(clower - '0') : ((clower >= 'a' && clower <= 'f') ? static_cast<unsigned char>(clower - 'a') : ((clower >= 'A' && clower <= 'F') ? static_cast<unsigned char>(clower - 'A') : 0));
const unsigned char upper = (cupper >= '0' && cupper <= '9') ? static_cast<unsigned char>(cupper - '0') : ((cupper >= 'a' && cupper <= 'f') ? static_cast<unsigned char>(cupper - 'a') : ((cupper >= 'A' && cupper <= 'F') ? static_cast<unsigned char>(cupper - 'A') : 0));
const char dch = static_cast<char>(lower | (upper << 4));
ret.push_back(dch);
}
else
{
ret.push_back(c);
}
}
return std::string(str);
}
std::string HTTPDownloader::GetExtensionForContentType(const std::string& content_type)
{
// Based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
static constexpr const char* table[][2] = {
{"audio/aac", "aac"},
{"application/x-abiword", "abw"},
{"application/x-freearc", "arc"},
{"image/avif", "avif"},
{"video/x-msvideo", "avi"},
{"application/vnd.amazon.ebook", "azw"},
{"application/octet-stream", "bin"},
{"image/bmp", "bmp"},
{"application/x-bzip", "bz"},
{"application/x-bzip2", "bz2"},
{"application/x-cdf", "cda"},
{"application/x-csh", "csh"},
{"text/css", "css"},
{"text/csv", "csv"},
{"application/msword", "doc"},
{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"},
{"application/vnd.ms-fontobject", "eot"},
{"application/epub+zip", "epub"},
{"application/gzip", "gz"},
{"image/gif", "gif"},
{"text/html", "htm"},
{"image/vnd.microsoft.icon", "ico"},
{"text/calendar", "ics"},
{"application/java-archive", "jar"},
{"image/jpeg", "jpg"},
{"text/javascript", "js"},
{"application/json", "json"},
{"application/ld+json", "jsonld"},
{"audio/midi audio/x-midi", "mid"},
{"text/javascript", "mjs"},
{"audio/mpeg", "mp3"},
{"video/mp4", "mp4"},
{"video/mpeg", "mpeg"},
{"application/vnd.apple.installer+xml", "mpkg"},
{"application/vnd.oasis.opendocument.presentation", "odp"},
{"application/vnd.oasis.opendocument.spreadsheet", "ods"},
{"application/vnd.oasis.opendocument.text", "odt"},
{"audio/ogg", "oga"},
{"video/ogg", "ogv"},
{"application/ogg", "ogx"},
{"audio/opus", "opus"},
{"font/otf", "otf"},
{"image/png", "png"},
{"application/pdf", "pdf"},
{"application/x-httpd-php", "php"},
{"application/vnd.ms-powerpoint", "ppt"},
{"application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx"},
{"application/vnd.rar", "rar"},
{"application/rtf", "rtf"},
{"application/x-sh", "sh"},
{"image/svg+xml", "svg"},
{"application/x-tar", "tar"},
{"image/tiff", "tif"},
{"video/mp2t", "ts"},
{"font/ttf", "ttf"},
{"text/plain", "txt"},
{"application/vnd.visio", "vsd"},
{"audio/wav", "wav"},
{"audio/webm", "weba"},
{"video/webm", "webm"},
{"image/webp", "webp"},
{"font/woff", "woff"},
{"font/woff2", "woff2"},
{"application/xhtml+xml", "xhtml"},
{"application/vnd.ms-excel", "xls"},
{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"},
{"application/xml", "xml"},
{"text/xml", "xml"},
{"application/vnd.mozilla.xul+xml", "xul"},
{"application/zip", "zip"},
{"video/3gpp", "3gp"},
{"audio/3gpp", "3gp"},
{"video/3gpp2", "3g2"},
{"audio/3gpp2", "3g2"},
{"application/x-7z-compressed", "7z"},
};
std::string ret;
for (size_t i = 0; i < std::size(table); i++)
{
if (StringUtil::compareNoCase(table[i][0], content_type))
{
ret = table[i][1];
break;
}
}
return ret;
}

106
common/HTTPDownloader.h Normal file
View File

@ -0,0 +1,106 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "common/Pcsx2Defs.h"
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <vector>
namespace Common
{
class HTTPDownloader
{
public:
enum : s32
{
HTTP_OK = 200
};
struct Request
{
using Data = std::vector<u8>;
using Callback = std::function<void(s32 status_code, std::string content_type, Data data)>;
enum class Type
{
Get,
Post,
};
enum class State
{
Pending,
Cancelled,
Started,
Receiving,
Complete,
};
HTTPDownloader* parent;
Callback callback;
std::string url;
std::string post_data;
std::string content_type;
Data data;
u64 start_time;
s32 status_code = 0;
u32 content_length = 0;
Type type = Type::Get;
std::atomic<State> state{State::Pending};
};
HTTPDownloader();
virtual ~HTTPDownloader();
static std::unique_ptr<HTTPDownloader> Create(const char* user_agent = DEFAULT_USER_AGENT);
static std::string URLEncode(const std::string_view& str);
static std::string URLDecode(const std::string_view& str);
static std::string GetExtensionForContentType(const std::string& content_type);
void SetTimeout(float timeout);
void SetMaxActiveRequests(u32 max_active_requests);
void CreateRequest(std::string url, Request::Callback callback);
void CreatePostRequest(std::string url, std::string post_data, Request::Callback callback);
void PollRequests();
void WaitForAllRequests();
bool HasAnyRequests();
static const char DEFAULT_USER_AGENT[];
protected:
virtual Request* InternalCreateRequest() = 0;
virtual void InternalPollRequests() = 0;
virtual bool StartRequest(Request* request) = 0;
virtual void CloseRequest(Request* request) = 0;
void LockedAddRequest(Request* request);
u32 LockedGetActiveRequestCount();
void LockedPollRequests(std::unique_lock<std::mutex>& lock);
float m_timeout;
u32 m_max_active_requests;
std::mutex m_pending_http_request_lock;
std::vector<Request*> m_pending_http_requests;
};
} // namespace Common

View File

@ -0,0 +1,185 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "common/PrecompiledHeader.h"
#include "common/HTTPDownloaderCurl.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include <algorithm>
#include <functional>
#include <pthread.h>
#include <signal.h>
using namespace Common;
HTTPDownloaderCurl::HTTPDownloaderCurl()
: HTTPDownloader()
{
}
HTTPDownloaderCurl::~HTTPDownloaderCurl() = default;
std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(const char* user_agent)
{
std::unique_ptr<HTTPDownloaderCurl> instance(std::make_unique<HTTPDownloaderCurl>());
if (!instance->Initialize(user_agent))
return {};
return instance;
}
static bool s_curl_initialized = false;
static std::once_flag s_curl_initialized_once_flag;
bool HTTPDownloaderCurl::Initialize(const char* user_agent)
{
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)
{
Console.Error("curl_global_init() failed");
return false;
}
}
m_user_agent = user_agent;
m_thread_pool = std::make_unique<cb::ThreadPool>(m_max_active_requests);
return true;
}
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();
// Apparently OpenSSL can fire SIGPIPE...
sigset_t old_block_mask = {};
sigset_t new_block_mask = {};
sigemptyset(&old_block_mask);
sigemptyset(&new_block_mask);
sigaddset(&new_block_mask, SIGPIPE);
if (pthread_sigmask(SIG_BLOCK, &new_block_mask, &old_block_mask) != 0)
Console.Warning("Failed to block SIGPIPE");
req->start_time = Common::Timer::GetCurrentValue();
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);
char* content_type = nullptr;
if (!curl_easy_getinfo(req->handle, CURLINFO_CONTENT_TYPE, &content_type) && content_type)
req->content_type = content_type;
DevCon.WriteLn("Request for '%s' returned status code %d and %zu bytes", req->url.c_str(), req->status_code,
req->data.size());
}
else
{
Console.Error("Request for '%s' returned %d", req->url.c_str(), ret);
}
curl_easy_cleanup(req->handle);
if (pthread_sigmask(SIG_UNBLOCK, &new_block_mask, &old_block_mask) != 0)
Console.Warning("Failed to unblock SIGPIPE");
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);
curl_easy_setopt(req->handle, CURLOPT_NOSIGNAL, 1);
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());
}
DbgCon.WriteLn("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
req->start_time = Common::Timer::GetCurrentValue();
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);
}

View File

@ -0,0 +1,54 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "common/HTTPDownloader.h"
#include "common/ThreadPool.h"
#include <atomic>
#include <memory>
#include <mutex>
#include <curl/curl.h>
namespace Common
{
class HTTPDownloaderCurl final : public HTTPDownloader
{
public:
HTTPDownloaderCurl();
~HTTPDownloaderCurl() override;
bool Initialize(const char* user_agent);
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::string m_user_agent;
std::unique_ptr<cb::ThreadPool> m_thread_pool;
std::mutex m_cancel_mutex;
};
} // namespace Common

View File

@ -0,0 +1,333 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "common/PrecompiledHeader.h"
#include "common/HTTPDownloaderWinHTTP.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include <VersionHelpers.h>
#include <algorithm>
#pragma comment(lib, "winhttp.lib")
using namespace Common;
HTTPDownloaderWinHttp::HTTPDownloaderWinHttp()
: HTTPDownloader()
{
}
HTTPDownloaderWinHttp::~HTTPDownloaderWinHttp()
{
if (m_hSession)
{
WinHttpSetStatusCallback(m_hSession, nullptr, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, NULL);
WinHttpCloseHandle(m_hSession);
}
}
std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(const char* user_agent)
{
std::unique_ptr<HTTPDownloaderWinHttp> instance(std::make_unique<HTTPDownloaderWinHttp>());
if (!instance->Initialize(user_agent))
return {};
return instance;
}
bool HTTPDownloaderWinHttp::Initialize(const char* user_agent)
{
// WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY is not supported before Win8.1.
const DWORD dwAccessType =
IsWindows8Point1OrGreater() ? WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY : WINHTTP_ACCESS_TYPE_DEFAULT_PROXY;
m_hSession = WinHttpOpen(StringUtil::UTF8StringToWideString(user_agent).c_str(), dwAccessType, nullptr, nullptr,
WINHTTP_FLAG_ASYNC);
if (m_hSession == NULL)
{
Console.Error("WinHttpOpen() failed: %u", GetLastError());
return false;
}
const DWORD notification_flags = WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | WINHTTP_CALLBACK_FLAG_REQUEST_ERROR |
WINHTTP_CALLBACK_FLAG_HANDLES | WINHTTP_CALLBACK_FLAG_SECURE_FAILURE;
if (WinHttpSetStatusCallback(m_hSession, HTTPStatusCallback, notification_flags, NULL) ==
WINHTTP_INVALID_STATUS_CALLBACK)
{
Console.Error("WinHttpSetStatusCallback() failed: %u", GetLastError());
return false;
}
return true;
}
void CALLBACK HTTPDownloaderWinHttp::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;
pxAssert(hRequest == req->hRequest);
HTTPDownloaderWinHttp* parent = static_cast<HTTPDownloaderWinHttp*>(req->parent);
std::unique_lock<std::mutex> lock(parent->m_pending_http_request_lock);
pxAssertRel(std::none_of(parent->m_pending_http_requests.begin(), parent->m_pending_http_requests.end(),
[req](HTTPDownloader::Request* it) { return it == req; }),
"Request is not pending at close time");
// we can clean up the connection as well
pxAssert(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);
Console.Error("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:
{
DbgCon.WriteLn("SendRequest complete");
if (!WinHttpReceiveResponse(hRequest, nullptr))
{
Console.Error("WinHttpReceiveResponse() failed: %u", GetLastError());
req->status_code = -1;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
{
DbgCon.WriteLn("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))
{
Console.Error("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)
Console.Warning("WinHttpQueryHeaders() for content length failed: %u", GetLastError());
req->content_length = 0;
}
DWORD content_type_length = 0;
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX,
WINHTTP_NO_OUTPUT_BUFFER, &content_type_length, WINHTTP_NO_HEADER_INDEX) &&
GetLastError() == ERROR_INSUFFICIENT_BUFFER && content_type_length >= sizeof(content_type_length))
{
std::wstring content_type_wstring;
content_type_wstring.resize((content_type_length / sizeof(wchar_t)) - 1);
if (WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX,
content_type_wstring.data(), &content_type_length, WINHTTP_NO_HEADER_INDEX))
{
req->content_type = StringUtil::WideStringToUTF8String(content_type_wstring);
}
}
DbgCon.WriteLn("Status code %d, content-length is %u, content-type is %s", req->status_code, req->content_length,
req->content_type.c_str());
req->data.reserve(req->content_length);
req->state = Request::State::Receiving;
// start reading
if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING)
{
Console.Error("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
DbgCon.WriteLn("End of request '%s', %zu bytes received", req->url.c_str(), req->data.size());
req->state.store(Request::State::Complete);
return;
}
// start the transfer
DbgCon.WriteLn("%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)
{
Console.Error("WinHttpReadData() failed: %u", GetLastError());
req->status_code = -1;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
{
DbgCon.WriteLn("Read of %u complete", dwStatusInformationLength);
const u32 new_size = req->io_position + dwStatusInformationLength;
pxAssertRel(new_size <= req->data.size(), "HTTP overread occurred");
req->data.resize(new_size);
req->start_time = Common::Timer::GetCurrentValue();
if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING)
{
Console.Error("WinHttpQueryDataAvailable() failed: %u", GetLastError());
req->status_code = -1;
req->state.store(Request::State::Complete);
}
return;
}
default:
// unhandled, ignore
return;
}
}
HTTPDownloader::Request* HTTPDownloaderWinHttp::InternalCreateRequest()
{
Request* req = new Request();
return req;
}
void HTTPDownloaderWinHttp::InternalPollRequests()
{
// noop - it uses windows's worker threads
}
bool HTTPDownloaderWinHttp::StartRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
std::wstring host_name;
host_name.resize(req->url.size());
req->object_name.resize(req->url.size());
URL_COMPONENTSW uc = {};
uc.dwStructSize = sizeof(uc);
uc.lpszHostName = host_name.data();
uc.dwHostNameLength = static_cast<DWORD>(host_name.size());
uc.lpszUrlPath = req->object_name.data();
uc.dwUrlPathLength = static_cast<DWORD>(req->object_name.size());
const std::wstring url_wide(StringUtil::UTF8StringToWideString(req->url));
if (!WinHttpCrackUrl(url_wide.c_str(), static_cast<DWORD>(url_wide.size()), 0, &uc))
{
Console.Error("WinHttpCrackUrl() failed: %u", GetLastError());
req->callback(-1, std::string(), Request::Data());
delete req;
return false;
}
host_name.resize(uc.dwHostNameLength);
req->object_name.resize(uc.dwUrlPathLength);
req->hConnection = WinHttpConnect(m_hSession, host_name.c_str(), uc.nPort, 0);
if (!req->hConnection)
{
Console.Error("Failed to start HTTP request for '%s': %u", req->url.c_str(), GetLastError());
req->callback(-1, std::string(), Request::Data());
delete req;
return false;
}
const DWORD request_flags = uc.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0;
req->hRequest =
WinHttpOpenRequest(req->hConnection, (req->type == HTTPDownloader::Request::Type::Post) ? L"POST" : L"GET",
req->object_name.c_str(), NULL, NULL, NULL, request_flags);
if (!req->hRequest)
{
Console.Error("WinHttpOpenRequest() failed: %u", GetLastError());
WinHttpCloseHandle(req->hConnection);
return false;
}
BOOL result;
if (req->type == HTTPDownloader::Request::Type::Post)
{
const std::wstring_view additional_headers(L"Content-Type: application/x-www-form-urlencoded\r\n");
result = WinHttpSendRequest(req->hRequest, additional_headers.data(), static_cast<DWORD>(additional_headers.size()),
req->post_data.data(), static_cast<DWORD>(req->post_data.size()),
static_cast<DWORD>(req->post_data.size()), reinterpret_cast<DWORD_PTR>(req));
}
else
{
result = WinHttpSendRequest(req->hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0,
reinterpret_cast<DWORD_PTR>(req));
}
if (!result && GetLastError() != ERROR_IO_PENDING)
{
Console.Error("WinHttpSendRequest() failed: %u", GetLastError());
req->status_code = -1;
req->state.store(Request::State::Complete);
}
DevCon.WriteLn("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
req->start_time = Common::Timer::GetCurrentValue();
return true;
}
void HTTPDownloaderWinHttp::CloseRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
if (req->hRequest != NULL)
{
// req will be freed by the callback.
// the callback can fire immediately here if there's nothing running async, so don't touch req afterwards
WinHttpCloseHandle(req->hRequest);
return;
}
if (req->hConnection != NULL)
WinHttpCloseHandle(req->hConnection);
delete req;
}

View File

@ -0,0 +1,53 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "common/HTTPDownloader.h"
#include "common/RedtapeWindows.h"
#include <winhttp.h>
namespace Common
{
class HTTPDownloaderWinHttp final : public HTTPDownloader
{
public:
HTTPDownloaderWinHttp();
~HTTPDownloaderWinHttp() override;
bool Initialize(const char* user_agent);
protected:
Request* InternalCreateRequest() override;
void InternalPollRequests() override;
bool StartRequest(HTTPDownloader::Request* request) override;
void CloseRequest(HTTPDownloader::Request* request) override;
private:
struct Request : HTTPDownloader::Request
{
std::wstring object_name;
HINTERNET hConnection = NULL;
HINTERNET hRequest = NULL;
u32 io_position = 0;
};
static void CALLBACK HTTPStatusCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus,
LPVOID lpvStatusInformation, DWORD dwStatusInformationLength);
HINTERNET m_hSession = NULL;
};
} // namespace Common

View File

@ -38,8 +38,8 @@ namespace Path
void Canonicalize(std::string* path);
/// Sanitizes a filename for use in a filesystem.
void SanitizeFileName(char* Destination, u32 cbDestination, const char* FileName, bool StripSlashes /* = true */);
void SanitizeFileName(std::string& Destination, bool StripSlashes = true);
std::string SanitizeFileName(const std::string_view& str, bool strip_slashes = true);
void SanitizeFileName(std::string* str, bool strip_slashes = true);
/// Returns true if the specified path is an absolute path (C:\Path on Windows or /path on Unix).
bool IsAbsolute(const std::string_view& path);

View File

@ -370,6 +370,88 @@ namespace StringUtil
}
}
void EncodeAndAppendUTF8(std::string& s, char32_t ch)
{
if (ch <= 0x7F)
{
s.push_back(static_cast<char>(static_cast<u8>(ch)));
}
else if (ch <= 0x07FF)
{
s.push_back(static_cast<char>(static_cast<u8>(0xc0 | static_cast<u8>((ch >> 6) & 0x1f))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>((ch & 0x3f)))));
}
else if (ch <= 0xFFFF)
{
s.push_back(static_cast<char>(static_cast<u8>(0xe0 | static_cast<u8>(((ch >> 12) & 0x0f)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>(((ch >> 6) & 0x3f)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>((ch & 0x3f)))));
}
else if (ch <= 0x10FFFF)
{
s.push_back(static_cast<char>(static_cast<u8>(0xf0 | static_cast<u8>(((ch >> 18) & 0x07)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>(((ch >> 12) & 0x3f)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>(((ch >> 6) & 0x3f)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>((ch & 0x3f)))));
}
else
{
s.push_back(static_cast<char>(0xefu));
s.push_back(static_cast<char>(0xbfu));
s.push_back(static_cast<char>(0xbdu));
}
}
size_t DecodeUTF8(const void* bytes, size_t length, char32_t* ch)
{
const u8* s = reinterpret_cast<const u8*>(bytes);
if (s[0] < 0x80)
{
*ch = s[0];
return 1;
}
else if ((s[0] & 0xe0) == 0xc0)
{
if (length < 2)
goto invalid;
*ch = static_cast<char32_t>((static_cast<u32>(s[0] & 0x1f) << 6) | (static_cast<u32>(s[1] & 0x3f) << 0));
return 2;
}
else if ((s[0] & 0xf0) == 0xe0)
{
if (length < 3)
goto invalid;
*ch = static_cast<char32_t>((static_cast<u32>(s[0] & 0x0f) << 12) | (static_cast<u32>(s[1] & 0x3f) << 6) |
(static_cast<u32>(s[2] & 0x3f) << 0));
return 3;
}
else if ((s[0] & 0xf8) == 0xf0 && (s[0] <= 0xf4))
{
if (length < 4)
goto invalid;
*ch = static_cast<char32_t>((static_cast<u32>(s[0] & 0x07) << 18) | (static_cast<u32>(s[1] & 0x3f) << 12) |
(static_cast<u32>(s[2] & 0x3f) << 6) | (static_cast<u32>(s[3] & 0x3f) << 0));
return 4;
}
invalid:
*ch = 0xFFFFFFFFu;
return 1;
}
size_t DecodeUTF8(const std::string_view& str, size_t offset, char32_t* ch)
{
return DecodeUTF8(str.data() + offset, str.length() - offset, ch);
}
size_t DecodeUTF8(const std::string& str, size_t offset, char32_t* ch)
{
return DecodeUTF8(str.data() + offset, str.length() - offset, ch);
}
#ifdef _WIN32
std::wstring UTF8StringToWideString(const std::string_view& str)
{

View File

@ -169,7 +169,7 @@ namespace StringUtil
std::vector<std::string_view> SplitString(const std::string_view& str, char delimiter, bool skip_empty = true);
/// Joins a string together using the specified delimiter.
template<typename T>
template <typename T>
static inline std::string JoinString(const T& start, const T& end, char delimiter)
{
std::string ret;
@ -204,6 +204,15 @@ namespace StringUtil
/// Appends a UTF-16/UTF-32 codepoint to a UTF-8 string.
void AppendUTF16CharacterToUTF8(std::string& s, u16 ch);
/// Appends a UTF-16/UTF-32 codepoint to a UTF-8 string.
void EncodeAndAppendUTF8(std::string& s, char32_t ch);
/// Decodes UTF-8 to a single codepoint, updating the position parameter.
/// Returns the number of bytes the codepoint took in the original string.
size_t DecodeUTF8(const void* bytes, size_t length, char32_t* ch);
size_t DecodeUTF8(const std::string_view& str, size_t offset, char32_t* ch);
size_t DecodeUTF8(const std::string& str, size_t offset, char32_t* ch);
/// Strided memcpy/memcmp.
static inline void StrideMemCpy(void* dst, std::size_t dst_stride, const void* src, std::size_t src_stride,
std::size_t copy_size, std::size_t count)

137
common/ThreadPool.cpp Normal file
View File

@ -0,0 +1,137 @@
/*
* MIT License
*
* Copyright (c) 2022 Colion Braley
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// From https://raw.githubusercontent.com/cbraley/threadpool/master/src/thread_pool.cc
#include "common/PrecompiledHeader.h"
#include "common/ThreadPool.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

255
common/ThreadPool.h Normal file
View File

@ -0,0 +1,255 @@
/*
* MIT License
*
* Copyright (c) 2022 Colion Braley
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// From https://raw.githubusercontent.com/cbraley/threadpool/master/src/thread_pool.h
#pragma once
// 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 || defined(_MSC_VER)
#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

View File

@ -67,6 +67,13 @@
<ClCompile Include="GL\StreamBuffer.cpp" />
<ClCompile Include="FileSystem.cpp" />
<ClCompile Include="Image.cpp" />
<ClCompile Include="HTTPDownloader.cpp" />
<ClCompile Include="HTTPDownloaderCurl.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Devel|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="HTTPDownloaderWinHTTP.cpp" />
<ClCompile Include="MD5Digest.cpp" />
<ClCompile Include="ProgressCallback.cpp" />
<ClCompile Include="StackWalker.cpp" />
@ -141,6 +148,13 @@
<ClInclude Include="HashCombine.h" />
<ClInclude Include="Image.h" />
<ClInclude Include="LRUCache.h" />
<ClInclude Include="HTTPDownloader.h" />
<ClInclude Include="HTTPDownloaderCurl.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Devel|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="HTTPDownloaderWinHTTP.h" />
<ClInclude Include="MD5Digest.h" />
<ClInclude Include="ProgressCallback.h" />
<ClInclude Include="ScopedGuard.h" />
@ -159,6 +173,7 @@
<ClInclude Include="PrecompiledHeader.h" />
<ClInclude Include="RedtapeWindows.h" />
<ClInclude Include="SafeArray.h" />
<ClInclude Include="ThreadPool.h" />
<ClInclude Include="Timer.h" />
<ClInclude Include="Vulkan\Builders.h" />
<ClInclude Include="Vulkan\Context.h" />
@ -217,4 +232,4 @@
<ImportGroup Label="ExtensionTargets">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
</ImportGroup>
</Project>
</Project>

View File

@ -187,6 +187,15 @@
<ClCompile Include="Image.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HTTPDownloader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HTTPDownloaderCurl.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HTTPDownloaderWinHTTP.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="AlignedMalloc.h">
@ -441,6 +450,18 @@
<ClInclude Include="Easing.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HTTPDownloader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HTTPDownloaderCurl.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HTTPDownloaderWinHTTP.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ThreadPool.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Source Files">
@ -487,4 +508,4 @@
<Filter>Source Files</Filter>
</MASM>
</ItemGroup>
</Project>
</Project>

View File

@ -17,6 +17,9 @@ target_sources(pcsx2-qt PRIVATE
AutoUpdaterDialog.cpp
AutoUpdaterDialog.h
AutoUpdaterDialog.ui
CoverDownloadDialog.cpp
CoverDownloadDialog.h
CoverDownloadDialog.ui
DisplayWidget.cpp
DisplayWidget.h
EarlyHardwareCheck.cpp
@ -28,6 +31,8 @@ target_sources(pcsx2-qt PRIVATE
QtHost.cpp
QtHost.h
QtKeyCodes.cpp
QtProgressCallback.cpp
QtProgressCallback.h
QtUtils.cpp
QtUtils.h
SettingWidgetBinder.h

View File

@ -0,0 +1,139 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "common/Assertions.h"
#include "pcsx2/Frontend/GameList.h"
#include "CoverDownloadDialog.h"
CoverDownloadDialog::CoverDownloadDialog(QWidget* parent /*= nullptr*/)
: QDialog(parent)
{
m_ui.setupUi(this);
m_ui.coverIcon->setPixmap(QIcon::fromTheme("image-fill").pixmap(32));
updateEnabled();
connect(m_ui.start, &QPushButton::clicked, this, &CoverDownloadDialog::onStartClicked);
connect(m_ui.close, &QPushButton::clicked, this, &CoverDownloadDialog::onCloseClicked);
connect(m_ui.urls, &QTextEdit::textChanged, this, &CoverDownloadDialog::updateEnabled);
}
CoverDownloadDialog::~CoverDownloadDialog()
{
pxAssert(!m_thread);
}
void CoverDownloadDialog::closeEvent(QCloseEvent* ev)
{
cancelThread();
}
void CoverDownloadDialog::onDownloadStatus(const QString& text)
{
m_ui.status->setText(text);
}
void CoverDownloadDialog::onDownloadProgress(int value, int range)
{
// Limit to once every five seconds, otherwise it's way too flickery.
// Ideally in the future we'd have some way to invalidate only a single cover.
if (m_last_refresh_time.GetTimeSeconds() >= 5.0f)
{
emit coverRefreshRequested();
m_last_refresh_time.Reset();
}
if (range != m_ui.progress->maximum())
m_ui.progress->setMaximum(range);
m_ui.progress->setValue(value);
}
void CoverDownloadDialog::onDownloadComplete()
{
emit coverRefreshRequested();
if (m_thread)
{
m_thread->join();
m_thread.reset();
}
updateEnabled();
m_ui.status->setText(tr("Download complete."));
}
void CoverDownloadDialog::onStartClicked()
{
if (m_thread)
cancelThread();
else
startThread();
}
void CoverDownloadDialog::onCloseClicked()
{
if (m_thread)
cancelThread();
done(0);
}
void CoverDownloadDialog::updateEnabled()
{
const bool running = static_cast<bool>(m_thread);
m_ui.start->setText(running ? tr("Stop") : tr("Start"));
m_ui.start->setEnabled(running || !m_ui.urls->toPlainText().isEmpty());
m_ui.close->setEnabled(!running);
m_ui.urls->setEnabled(!running);
}
void CoverDownloadDialog::startThread()
{
m_thread = std::make_unique<CoverDownloadThread>(this, m_ui.urls->toPlainText(), m_ui.useSerialFileNames->isChecked());
m_last_refresh_time.Reset();
connect(m_thread.get(), &CoverDownloadThread::statusUpdated, this, &CoverDownloadDialog::onDownloadStatus);
connect(m_thread.get(), &CoverDownloadThread::progressUpdated, this, &CoverDownloadDialog::onDownloadProgress);
connect(m_thread.get(), &CoverDownloadThread::threadFinished, this, &CoverDownloadDialog::onDownloadComplete);
m_thread->start();
updateEnabled();
}
void CoverDownloadDialog::cancelThread()
{
if (!m_thread)
return;
m_thread->requestInterruption();
m_thread->join();
m_thread.reset();
}
CoverDownloadDialog::CoverDownloadThread::CoverDownloadThread(QWidget* parent, const QString& urls, bool use_serials)
: QtAsyncProgressThread(parent), m_use_serials(use_serials)
{
for (const QString& str : urls.split(QChar('\n')))
m_urls.push_back(str.toStdString());
}
CoverDownloadDialog::CoverDownloadThread::~CoverDownloadThread() = default;
void CoverDownloadDialog::CoverDownloadThread::runAsync()
{
GameList::DownloadCovers(m_urls, m_use_serials, this);
}

View File

@ -0,0 +1,69 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "common/Timer.h"
#include "common/Pcsx2Defs.h"
#include "QtProgressCallback.h"
#include "ui_CoverDownloadDialog.h"
#include <QtWidgets/QDialog>
#include <array>
#include <memory>
#include <string>
class CoverDownloadDialog final : public QDialog
{
Q_OBJECT
public:
CoverDownloadDialog(QWidget* parent = nullptr);
~CoverDownloadDialog();
Q_SIGNALS:
void coverRefreshRequested();
protected:
void closeEvent(QCloseEvent* ev);
private Q_SLOTS:
void onDownloadStatus(const QString& text);
void onDownloadProgress(int value, int range);
void onDownloadComplete();
void onStartClicked();
void onCloseClicked();
void updateEnabled();
private:
class CoverDownloadThread : public QtAsyncProgressThread
{
public:
CoverDownloadThread(QWidget* parent, const QString& urls, bool use_serials);
~CoverDownloadThread();
protected:
void runAsync() override;
private:
std::vector<std::string> m_urls;
bool m_use_serials;
};
void startThread();
void cancelThread();
Ui::CoverDownloadDialog m_ui;
std::unique_ptr<CoverDownloadThread> m_thread;
Common::Timer m_last_refresh_time;
};

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CoverDownloadDialog</class>
<widget class="QDialog" name="CoverDownloadDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>720</width>
<height>380</height>
</rect>
</property>
<property name="windowTitle">
<string>Download Covers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="coverIcon">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="resources/resources.qrc">:/icons/black/svg/image-fill.svg</pixmap>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>PCSX2 can automatically download covers for games which do not currently have a cover set. We do not host any cover images, the user must provide their own source for images.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${title}:&lt;/span&gt; Title of the game.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${filetitle}:&lt;/span&gt; Name component of the game's filename.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${serial}:&lt;/span&gt; Serial of the game.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Example:&lt;/span&gt; https://www.example-not-a-real-domain.com/covers/${serial}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="urls"/>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>By default, the downloaded covers will be saved with the game's title. If this is not desired, you can check the &quot;Use Serial File Names&quot; box below. Using serials instead of game titles will prevent conflicts when multiple regions of the same game are used.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="useSerialFileNames">
<property name="text">
<string>Use Serial File Names</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="status">
<property name="text">
<string>Waiting to start...</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QProgressBar" name="progress"/>
</item>
<item>
<widget class="QPushButton" name="start">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Start</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="close">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -247,7 +247,10 @@ bool DisplayWidget::event(QEvent* event)
// Forward text input to imgui.
if (ImGuiManager::WantsTextInput() && key_event->type() == QEvent::KeyPress)
{
const QString text(key_event->text());
// Don't forward backspace characters. We send the backspace as a normal key event,
// so if we send the character too, it double-deletes.
QString text(key_event->text());
text.remove(QChar('\b'));
if (!text.isEmpty())
ImGuiManager::AddTextInput(text.toStdString());
}

View File

@ -42,6 +42,7 @@
#include "AboutDialog.h"
#include "AutoUpdaterDialog.h"
#include "CoverDownloadDialog.h"
#include "DisplayWidget.h"
#include "GameList/GameListRefreshThread.h"
#include "GameList/GameListWidget.h"
@ -293,6 +294,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered);
connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered);
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() {
if (isShowingGameList())
@ -1497,6 +1499,13 @@ void MainWindow::onToolsOpenDataDirectoryTriggered()
QtUtils::OpenURL(this, QUrl::fromLocalFile(path));
}
void MainWindow::onToolsCoverDownloaderTriggered()
{
CoverDownloadDialog dlg(this);
connect(&dlg, &CoverDownloadDialog::coverRefreshRequested, m_game_list_widget, &GameListWidget::refreshGridCovers);
dlg.exec();
}
void MainWindow::updateTheme()
{
updateApplicationTheme();

View File

@ -148,6 +148,7 @@ private Q_SLOTS:
void onAboutActionTriggered();
void onCheckForUpdatesActionTriggered();
void onToolsOpenDataDirectoryTriggered();
void onToolsCoverDownloaderTriggered();
void updateTheme();
void onScreenshotActionTriggered();
void onSaveGSDumpActionTriggered();

View File

@ -192,6 +192,7 @@
<addaction name="actionInputRecControllerLogs"/>
</widget>
<addaction name="actionOpenDataDirectory"/>
<addaction name="actionCoverDownloader"/>
<addaction name="menuInput_Recording"/>
</widget>
<addaction name="menuSystem"/>
@ -849,6 +850,11 @@
<string>Big Picture</string>
</property>
</action>
<action name="actionCoverDownloader">
<property name="text">
<string>Cover Downloader...</string>
</property>
</action>
</widget>
<resources>
<include location="resources/resources.qrc"/>

View File

@ -52,6 +52,9 @@
#include <QtWidgets/QApplication>
#include <QtWidgets/QMessageBox>
#include <QtGui/QClipboard>
#include <QtGui/QInputMethod>
#include "fmt/core.h"
#include "DisplayWidget.h"
#include "GameList/GameListWidget.h"
@ -69,9 +72,9 @@ EmuThread* g_emu_thread = nullptr;
//////////////////////////////////////////////////////////////////////////
namespace QtHost {
static void PrintCommandLineVersion();
static void PrintCommandLineHelp(const char* progname);
static void PrintCommandLineHelp(const std::string_view& progname);
static std::shared_ptr<VMBootParameters>& AutoBoot(std::shared_ptr<VMBootParameters>& autoboot);
static bool ParseCommandLineOptions(int argc, char* argv[], std::shared_ptr<VMBootParameters>& autoboot);
static bool ParseCommandLineOptions(const QStringList& args, std::shared_ptr<VMBootParameters>& autoboot);
static bool InitializeConfig();
static void SaveSettings();
static void HookSignals();
@ -1405,6 +1408,20 @@ bool Host::CopyTextToClipboard(const std::string_view& text)
return true;
}
void Host::BeginTextInput()
{
QInputMethod* method = qApp->inputMethod();
if (method)
QMetaObject::invokeMethod(method, "show", Qt::QueuedConnection);
}
void Host::EndTextInput()
{
QInputMethod* method = qApp->inputMethod();
if (method)
QMetaObject::invokeMethod(method, "hide", Qt::QueuedConnection);
}
void Host::OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name)
{
emit g_emu_thread->onInputDeviceConnected(
@ -1468,10 +1485,10 @@ void QtHost::PrintCommandLineVersion()
std::fprintf(stderr, "\n");
}
void QtHost::PrintCommandLineHelp(const char* progname)
void QtHost::PrintCommandLineHelp(const std::string_view& progname)
{
PrintCommandLineVersion();
std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);
fmt::print(stderr, "Usage: {} [parameters] [--] [boot filename]\n", progname);
std::fprintf(stderr, "\n");
std::fprintf(stderr, " -help: Displays this information and exits.\n");
std::fprintf(stderr, " -version: Displays version information and exits.\n");
@ -1501,104 +1518,109 @@ std::shared_ptr<VMBootParameters>& QtHost::AutoBoot(std::shared_ptr<VMBootParame
return autoboot;
}
bool QtHost::ParseCommandLineOptions(int argc, char* argv[], std::shared_ptr<VMBootParameters>& autoboot)
bool QtHost::ParseCommandLineOptions(const QStringList& args, std::shared_ptr<VMBootParameters>& autoboot)
{
bool no_more_args = false;
for (int i = 1; i < argc; i++)
if (args.empty())
{
// Nothing to do here.
return true;
}
for (auto it = std::next(args.begin()); it != args.end(); ++it)
{
if (!no_more_args)
{
#define CHECK_ARG(str) !std::strcmp(argv[i], str)
#define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc))
#define CHECK_ARG(str) (*it == str)
#define CHECK_ARG_PARAM(str) (*it == str && std::next(it) != args.end())
if (CHECK_ARG("-help"))
if (CHECK_ARG(QStringLiteral("-help")))
{
PrintCommandLineHelp(argv[0]);
PrintCommandLineHelp(args.front().toStdString());
return false;
}
else if (CHECK_ARG("-version"))
else if (CHECK_ARG(QStringLiteral("-version")))
{
PrintCommandLineVersion();
return false;
}
else if (CHECK_ARG("-batch"))
else if (CHECK_ARG(QStringLiteral("-batch")))
{
s_batch_mode = true;
continue;
}
else if (CHECK_ARG("-nogui"))
else if (CHECK_ARG(QStringLiteral("-nogui")))
{
s_batch_mode = true;
s_nogui_mode = true;
continue;
}
else if (CHECK_ARG("-fastboot"))
else if (CHECK_ARG(QStringLiteral("-fastboot")))
{
AutoBoot(autoboot)->fast_boot = true;
continue;
}
else if (CHECK_ARG("-slowboot"))
else if (CHECK_ARG(QStringLiteral("-slowboot")))
{
AutoBoot(autoboot)->fast_boot = false;
continue;
}
else if (CHECK_ARG_PARAM("-state"))
else if (CHECK_ARG_PARAM(QStringLiteral("-state")))
{
AutoBoot(autoboot)->state_index = std::atoi(argv[++i]);
AutoBoot(autoboot)->state_index = (++it)->toInt();
continue;
}
else if (CHECK_ARG_PARAM("-statefile"))
else if (CHECK_ARG_PARAM(QStringLiteral("-statefile")))
{
AutoBoot(autoboot)->save_state = argv[++i];
AutoBoot(autoboot)->save_state = (++it)->toStdString();
continue;
}
else if (CHECK_ARG_PARAM("-elf"))
else if (CHECK_ARG_PARAM(QStringLiteral("-elf")))
{
AutoBoot(autoboot)->elf_override = argv[++i];
AutoBoot(autoboot)->elf_override = (++it)->toStdString();
continue;
}
else if (CHECK_ARG_PARAM("-disc"))
else if (CHECK_ARG_PARAM(QStringLiteral("-disc")))
{
AutoBoot(autoboot)->source_type = CDVD_SourceType::Disc;
AutoBoot(autoboot)->filename = argv[++i];
AutoBoot(autoboot)->filename = (++it)->toStdString();
continue;
}
else if (CHECK_ARG("-bios"))
else if (CHECK_ARG(QStringLiteral("-bios")))
{
AutoBoot(autoboot)->source_type = CDVD_SourceType::NoDisc;
continue;
}
else if (CHECK_ARG("-fullscreen"))
else if (CHECK_ARG(QStringLiteral("-fullscreen")))
{
AutoBoot(autoboot)->fullscreen = true;
s_start_fullscreen_ui_fullscreen = true;
continue;
}
else if (CHECK_ARG("-nofullscreen"))
else if (CHECK_ARG(QStringLiteral("-nofullscreen")))
{
AutoBoot(autoboot)->fullscreen = false;
continue;
}
else if (CHECK_ARG("-earlyconsolelog"))
else if (CHECK_ARG(QStringLiteral("-earlyconsolelog")))
{
CommonHost::InitializeEarlyConsole();
continue;
}
else if (CHECK_ARG("-bigpicture"))
else if (CHECK_ARG(QStringLiteral("-bigpicture")))
{
s_start_fullscreen_ui = true;
continue;
}
else if (CHECK_ARG("--"))
else if (CHECK_ARG(QStringLiteral("--")))
{
no_more_args = true;
continue;
}
else if (argv[i][0] == '-')
else if ((*it)[0] == '-')
{
CommonHost::InitializeEarlyConsole();
std::fprintf(stderr, "Unknown parameter: '%s'", argv[i]);
QMessageBox::critical(nullptr, QStringLiteral("Error"), QStringLiteral("Unknown parameter: '%1'").arg(*it));
return false;
}
@ -1609,7 +1631,7 @@ bool QtHost::ParseCommandLineOptions(int argc, char* argv[], std::shared_ptr<VMB
if (!AutoBoot(autoboot)->filename.empty())
AutoBoot(autoboot)->filename += ' ';
AutoBoot(autoboot)->filename += argv[i];
AutoBoot(autoboot)->filename += it->toStdString();
}
// check autoboot parameters, if we set something like fullscreen without a bios
@ -1679,7 +1701,7 @@ int main(int argc, char* argv[])
#endif
std::shared_ptr<VMBootParameters> autoboot;
if (!QtHost::ParseCommandLineOptions(argc, argv, autoboot))
if (!QtHost::ParseCommandLineOptions(app.arguments(), autoboot))
return EXIT_FAILURE;
// Bail out if we can't find any config.

View File

@ -0,0 +1,247 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "common/Assertions.h"
#include "QtProgressCallback.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtWidgets/QMessageBox>
#include <array>
QtModalProgressCallback::QtModalProgressCallback(QWidget* parent_widget, float show_delay)
: QObject(parent_widget)
, m_dialog(QString(), QString(), 0, 1, parent_widget)
, m_show_delay(show_delay)
{
m_dialog.setWindowTitle(tr("PCSX2"));
m_dialog.setMinimumSize(QSize(500, 0));
m_dialog.setModal(parent_widget != nullptr);
m_dialog.setAutoClose(false);
m_dialog.setAutoReset(false);
checkForDelayedShow();
}
QtModalProgressCallback::~QtModalProgressCallback() = default;
bool QtModalProgressCallback::IsCancelled() const
{
return m_dialog.wasCanceled();
}
void QtModalProgressCallback::SetCancellable(bool cancellable)
{
if (m_cancellable == cancellable)
return;
BaseProgressCallback::SetCancellable(cancellable);
m_dialog.setCancelButtonText(cancellable ? tr("Cancel") : QString());
}
void QtModalProgressCallback::SetTitle(const char* title)
{
m_dialog.setWindowTitle(QString::fromUtf8(title));
}
void QtModalProgressCallback::SetStatusText(const char* text)
{
BaseProgressCallback::SetStatusText(text);
checkForDelayedShow();
if (m_dialog.isVisible())
m_dialog.setLabelText(QString::fromUtf8(text));
}
void QtModalProgressCallback::SetProgressRange(u32 range)
{
BaseProgressCallback::SetProgressRange(range);
checkForDelayedShow();
if (m_dialog.isVisible())
m_dialog.setRange(0, m_progress_range);
}
void QtModalProgressCallback::SetProgressValue(u32 value)
{
BaseProgressCallback::SetProgressValue(value);
checkForDelayedShow();
if (m_dialog.isVisible() && static_cast<u32>(m_dialog.value()) != m_progress_range)
m_dialog.setValue(m_progress_value);
QCoreApplication::processEvents();
}
void QtModalProgressCallback::DisplayError(const char* message)
{
qWarning() << message;
}
void QtModalProgressCallback::DisplayWarning(const char* message)
{
qWarning() << message;
}
void QtModalProgressCallback::DisplayInformation(const char* message)
{
qWarning() << message;
}
void QtModalProgressCallback::DisplayDebugMessage(const char* message)
{
qWarning() << message;
}
void QtModalProgressCallback::ModalError(const char* message)
{
QMessageBox::critical(&m_dialog, tr("Error"), QString::fromUtf8(message));
}
bool QtModalProgressCallback::ModalConfirmation(const char* message)
{
return (QMessageBox::question(&m_dialog, tr("Question"), QString::fromUtf8(message), QMessageBox::Yes,
QMessageBox::No) == QMessageBox::Yes);
}
void QtModalProgressCallback::ModalInformation(const char* message)
{
QMessageBox::information(&m_dialog, tr("Information"), QString::fromUtf8(message));
}
void QtModalProgressCallback::checkForDelayedShow()
{
if (m_dialog.isVisible())
return;
if (m_show_timer.GetTimeSeconds() >= m_show_delay)
{
m_dialog.setRange(0, m_progress_range);
m_dialog.setValue(m_progress_value);
m_dialog.show();
}
}
QtAsyncProgressThread::QtAsyncProgressThread(QWidget* parent)
: QThread()
{
// NOTE: We deliberately don't set the thread parent, because otherwise we can't move it.
}
QtAsyncProgressThread::~QtAsyncProgressThread() = default;
bool QtAsyncProgressThread::IsCancelled() const
{
return isInterruptionRequested();
}
void QtAsyncProgressThread::SetCancellable(bool cancellable)
{
if (m_cancellable == cancellable)
return;
BaseProgressCallback::SetCancellable(cancellable);
}
void QtAsyncProgressThread::SetTitle(const char* title)
{
emit titleUpdated(QString::fromUtf8(title));
}
void QtAsyncProgressThread::SetStatusText(const char* text)
{
BaseProgressCallback::SetStatusText(text);
emit statusUpdated(QString::fromUtf8(text));
}
void QtAsyncProgressThread::SetProgressRange(u32 range)
{
BaseProgressCallback::SetProgressRange(range);
emit progressUpdated(static_cast<int>(m_progress_value), static_cast<int>(m_progress_range));
}
void QtAsyncProgressThread::SetProgressValue(u32 value)
{
BaseProgressCallback::SetProgressValue(value);
emit progressUpdated(static_cast<int>(m_progress_value), static_cast<int>(m_progress_range));
}
void QtAsyncProgressThread::DisplayError(const char* message)
{
qWarning() << message;
}
void QtAsyncProgressThread::DisplayWarning(const char* message)
{
qWarning() << message;
}
void QtAsyncProgressThread::DisplayInformation(const char* message)
{
qWarning() << message;
}
void QtAsyncProgressThread::DisplayDebugMessage(const char* message)
{
qWarning() << message;
}
void QtAsyncProgressThread::ModalError(const char* message)
{
QMessageBox::critical(parentWidget(), tr("Error"), QString::fromUtf8(message));
}
bool QtAsyncProgressThread::ModalConfirmation(const char* message)
{
return (QMessageBox::question(parentWidget(), tr("Question"), QString::fromUtf8(message), QMessageBox::Yes,
QMessageBox::No) == QMessageBox::Yes);
}
void QtAsyncProgressThread::ModalInformation(const char* message)
{
QMessageBox::information(parentWidget(), tr("Information"), QString::fromUtf8(message));
}
void QtAsyncProgressThread::start()
{
pxAssertRel(!isRunning(), "Async progress thread is not already running");
QThread::start();
moveToThread(this);
m_starting_thread = QThread::currentThread();
m_start_semaphore.release();
}
void QtAsyncProgressThread::join()
{
if (isRunning())
QThread::wait();
}
void QtAsyncProgressThread::run()
{
m_start_semaphore.acquire();
emit threadStarting();
runAsync();
emit threadFinished();
moveToThread(m_starting_thread);
}
QWidget* QtAsyncProgressThread::parentWidget() const
{
return qobject_cast<QWidget*>(parent());
}

View File

@ -0,0 +1,102 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "common/ProgressCallback.h"
#include "common/Timer.h"
#include <QtCore/QThread>
#include <QtCore/QSemaphore>
#include <QtWidgets/QProgressDialog>
#include <atomic>
class QtModalProgressCallback final : public QObject, public BaseProgressCallback
{
Q_OBJECT
public:
QtModalProgressCallback(QWidget* parent_widget, float show_delay = 0.0f);
~QtModalProgressCallback();
bool IsCancelled() const override;
void SetCancellable(bool cancellable) override;
void SetTitle(const char* title) override;
void SetStatusText(const char* text) override;
void SetProgressRange(u32 range) override;
void SetProgressValue(u32 value) override;
void DisplayError(const char* message) override;
void DisplayWarning(const char* message) override;
void DisplayInformation(const char* message) override;
void DisplayDebugMessage(const char* message) override;
void ModalError(const char* message) override;
bool ModalConfirmation(const char* message) override;
void ModalInformation(const char* message) override;
private:
void checkForDelayedShow();
QProgressDialog m_dialog;
Common::Timer m_show_timer;
float m_show_delay;
};
class QtAsyncProgressThread : public QThread, public BaseProgressCallback
{
Q_OBJECT
public:
QtAsyncProgressThread(QWidget* parent);
~QtAsyncProgressThread();
bool IsCancelled() const override;
void SetCancellable(bool cancellable) override;
void SetTitle(const char* title) override;
void SetStatusText(const char* text) override;
void SetProgressRange(u32 range) override;
void SetProgressValue(u32 value) override;
void DisplayError(const char* message) override;
void DisplayWarning(const char* message) override;
void DisplayInformation(const char* message) override;
void DisplayDebugMessage(const char* message) override;
void ModalError(const char* message) override;
bool ModalConfirmation(const char* message) override;
void ModalInformation(const char* message) override;
Q_SIGNALS:
void titleUpdated(const QString& title);
void statusUpdated(const QString& status);
void progressUpdated(int value, int range);
void threadStarting();
void threadFinished();
public Q_SLOTS:
void start();
void join();
protected:
virtual void runAsync() = 0;
void run() final;
private:
QWidget* parentWidget() const;
QSemaphore m_start_semaphore;
QThread* m_starting_thread = nullptr;
};

View File

@ -36,13 +36,9 @@ BIOSSettingsWidget::BIOSSettingsWidget(SettingsDialog* dialog, QWidget* parent)
m_ui.setupUi(this);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.fastBoot, "EmuCore", "EnableFastBoot", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.patchRegion, "EmuCore", "PatchBios", false);
SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.regionComboBox, "EmuCore", "PatchRegion", BiosZoneStrings, BiosZoneBytes, BiosZoneBytes[0]);
SettingWidgetBinder::BindWidgetToFolderSetting(sif, m_ui.searchDirectory, m_ui.browseSearchDirectory, m_ui.openSearchDirectory,
m_ui.resetSearchDirectory, "Folders", "Bios", Path::Combine(EmuFolders::DataRoot, "bios"));
dialog->registerWidgetHelp(m_ui.patchRegion, tr("Patch Region"), tr("Unchecked"),
tr("Patches the BIOS region byte in ROM. Not recommended unless you really know what you're doing."));
dialog->registerWidgetHelp(m_ui.fastBoot, tr("Fast Boot"), tr("Checked"),
tr("Patches the BIOS to skip the console's boot animation."));
@ -51,9 +47,6 @@ BIOSSettingsWidget::BIOSSettingsWidget(SettingsDialog* dialog, QWidget* parent)
connect(m_ui.searchDirectory, &QLineEdit::textChanged, this, &BIOSSettingsWidget::refreshList);
connect(m_ui.refresh, &QPushButton::clicked, this, &BIOSSettingsWidget::refreshList);
connect(m_ui.fileList, &QTreeWidget::currentItemChanged, this, &BIOSSettingsWidget::listItemChanged);
connect(m_ui.patchRegion, &QCheckBox::clicked, this, [&] { m_ui.regionComboBox->setEnabled(m_ui.patchRegion->isChecked()); });
m_ui.regionComboBox->setEnabled(m_ui.patchRegion->isChecked());
}
BIOSSettingsWidget::~BIOSSettingsWidget()

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>618</width>
<height>439</height>
<height>408</height>
</rect>
</property>
<property name="windowTitle">
@ -134,41 +134,14 @@
<property name="title">
<string>Options and Patches</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0">
<widget class="QCheckBox" name="patchRegion">
<property name="text">
<string>Patch Region</string>
</property>
</widget>
</item>
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="fastBoot">
<property name="text">
<string>Fast Boot</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="regionComboBox">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>

View File

@ -520,6 +520,19 @@ void ControllerCustomSettingsWidget::createSettingWidgets(ControllerBindingWidge
}
break;
case PAD::ControllerSettingInfo::Type::IntegerList:
{
QComboBox* cb = new QComboBox(widget_parent);
cb->setObjectName(QString::fromUtf8(si.name));
for (u32 i = 0; si.options[i] != nullptr; i++)
cb->addItem(qApp->translate(cinfo->name, si.options[i]));
SettingWidgetBinder::BindWidgetToIntSetting(sif, cb, section, std::move(key_name), si.IntegerDefaultValue(), si.IntegerMinValue());
layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), widget_parent), current_row, 0);
layout->addWidget(cb, current_row, 1, 1, 3);
current_row++;
}
break;
case PAD::ControllerSettingInfo::Type::Float:
{
QDoubleSpinBox* sb = new QDoubleSpinBox(widget_parent);
@ -605,6 +618,14 @@ void ControllerCustomSettingsWidget::restoreDefaults()
}
break;
case PAD::ControllerSettingInfo::Type::IntegerList:
{
QComboBox* widget = findChild<QComboBox*>(QString::fromStdString(si.name));
if (widget)
widget->setCurrentIndex(si.IntegerDefaultValue() - si.IntegerMinValue());
}
break;
case PAD::ControllerSettingInfo::Type::Float:
{
QDoubleSpinBox* widget = findChild<QDoubleSpinBox*>(QString::fromStdString(si.name));

View File

@ -36,8 +36,6 @@ ControllerGlobalSettingsWidget::ControllerGlobalSettingsWidget(QWidget* parent,
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableMouseMapping, "UI", "EnableMouseMapping", false);
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.multitapPort1, "Pad", "MultitapPort1", false);
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.multitapPort2, "Pad", "MultitapPort2", false);
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.pointerXInvert, "Pad", "PointerXInvert", false);
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.pointerYInvert, "Pad", "PointerYInvert", false);
ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, m_ui.pointerXScale, "Pad", "PointerXScale", 8.0f);
ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, m_ui.pointerYScale, "Pad", "PointerYScale", 8.0f);

View File

@ -26,6 +26,31 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="1" rowspan="6">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Detected Devices</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QListWidget" name="deviceList">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
@ -85,142 +110,39 @@
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Mouse/Pointer Source</string>
<item row="5" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="1" column="2">
<widget class="QLabel" name="pointerXScaleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>45</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0">
<widget class="QGroupBox" name="profileSettings">
<property name="title">
<string>Profile Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>10</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSlider" name="pointerXScale">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QCheckBox" name="pointerYInvert">
<property name="text">
<string>Invert</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSlider" name="pointerYScale">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="pointerYScaleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QCheckBox" name="pointerXInvert">
<property name="text">
<string>Invert</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Using raw input improves precision when you bind controller sticks to the mouse pointer. Also enables multiple mice to be used.</string>
<string>When this option is enabled, hotkeys can be set in this input profile, and will be used instead of the global hotkeys. By default, hotkeys are always shared between all profiles.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Vertical Sensitivity:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<widget class="QCheckBox" name="useProfileHotkeyBindings">
<property name="text">
<string>Horizontal Sensitivity:</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="4">
<widget class="QCheckBox" name="enableMouseMapping">
<property name="text">
<string>Enable Mouse Mapping</string>
<string>Use Per-Profile Hotkeys</string>
</property>
</widget>
</item>
@ -260,16 +182,16 @@
</layout>
</widget>
</item>
<item row="4" column="0">
<widget class="QGroupBox" name="profileSettings">
<item row="2" column="0">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Profile Settings</string>
<string>Mouse/Pointer Source</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_7">
<property name="text">
<string>When this option is enabled, hotkeys can be set in this input profile, and will be used instead of the global hotkeys. By default, hotkeys are always shared between all profiles.</string>
<string>Using raw input improves precision when you bind controller sticks to the mouse pointer. Also enables multiple mice to be used.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
@ -277,50 +199,126 @@
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="useProfileHotkeyBindings">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Use Per-Profile Hotkeys</string>
<string>Horizontal Sensitivity:</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="5" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>45</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="1" rowspan="6">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Detected Devices</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QListWidget" name="deviceList">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSlider" name="pointerXScale">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pointerXScaleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Vertical Sensitivity:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QSlider" name="pointerYScale">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pointerYScaleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="enableMouseMapping">
<property name="text">
<string>Enable Mouse Mapping</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>

View File

@ -48,6 +48,9 @@ SystemSettingsWidget::SystemSettingsWidget(SettingsDialog* dialog, QWidget* pare
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.vuRoundingMode, "EmuCore/CPU", "VU.Roundmode", 3);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.fastCDVD, "EmuCore/Speedhacks", "fastCDVD", false);
// Allow for FastCDVD for per-game settings only
m_ui.fastCDVD->setEnabled(m_dialog->isPerGameSettings());
if (m_dialog->isPerGameSettings())
{
m_ui.eeCycleRate->insertItem(

View File

@ -138,6 +138,7 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="EarlyHardwareCheck.cpp" />
<ClCompile Include="QtProgressCallback.cpp" />
<ClCompile Include="Settings\FolderSettingsWidget.cpp" />
<ClCompile Include="Tools\InputRecording\NewInputRecordingDlg.cpp" />
<ClCompile Include="Settings\BIOSSettingsWidget.cpp" />
@ -170,6 +171,7 @@
<ClCompile Include="GameList\GameListWidget.cpp" />
<ClCompile Include="AboutDialog.cpp" />
<ClCompile Include="AutoUpdaterDialog.cpp" />
<ClCompile Include="CoverDownloadDialog.cpp" />
<ClCompile Include="DisplayWidget.cpp" />
<ClCompile Include="QtHost.cpp" />
<ClCompile Include="MainWindow.cpp" />
@ -198,6 +200,7 @@
<QtMoc Include="Settings\DEV9DnsHostDialog.h" />
<QtMoc Include="Settings\DEV9SettingsWidget.h" />
<QtMoc Include="Settings\DEV9UiCommon.h" />
<QtMoc Include="QtProgressCallback.h" />
<ClInclude Include="Settings\ControllerSettingWidgetBinder.h" />
<QtMoc Include="Settings\FolderSettingsWidget.h" />
<ClInclude Include="Settings\HddCreateQt.h" />
@ -217,6 +220,7 @@
<ClInclude Include="PrecompiledHeader.h" />
<QtMoc Include="AboutDialog.h" />
<QtMoc Include="AutoUpdaterDialog.h" />
<QtMoc Include="CoverDownloadDialog.h" />
<QtMoc Include="MainWindow.h" />
<QtMoc Include="DisplayWidget.h" />
</ItemGroup>
@ -256,9 +260,11 @@
<ClCompile Include="$(IntDir)GameList\moc_GameListWidget.cpp" />
<ClCompile Include="$(IntDir)moc_AboutDialog.cpp" />
<ClCompile Include="$(IntDir)moc_AutoUpdaterDialog.cpp" />
<ClCompile Include="$(IntDir)moc_CoverDownloadDialog.cpp" />
<ClCompile Include="$(IntDir)moc_DisplayWidget.cpp" />
<ClCompile Include="$(IntDir)moc_MainWindow.cpp" />
<ClCompile Include="$(IntDir)moc_QtHost.cpp" />
<ClCompile Include="$(IntDir)moc_QtProgressCallback.cpp" />
<ClCompile Include="$(IntDir)Tools\InputRecording\moc_NewInputRecordingDlg.cpp" />
<ClCompile Include="$(IntDir)qrc_resources.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
@ -343,6 +349,9 @@
<QtUi Include="AutoUpdaterDialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="CoverDownloadDialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="Tools\InputRecording\NewInputRecordingDlg.ui">
<FileType>Document</FileType>
</QtUi>

View File

@ -227,6 +227,14 @@
<ClCompile Include="$(IntDir)Settings\moc_FolderSettingsWidget.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="$(IntDir)moc_CoverDownloadDialog.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="CoverDownloadDialog.cpp" />
<ClCompile Include="QtProgressCallback.cpp" />
<ClCompile Include="$(IntDir)moc_QtProgressCallback.cpp">
<Filter>moc</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Manifest Include="..\pcsx2\windows\PCSX2.manifest">
@ -235,7 +243,6 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="PrecompiledHeader.h" />
<ClInclude Include="QtHost.h" />
<ClInclude Include="QtUtils.h" />
<ClInclude Include="SettingWidgetBinder.h" />
<ClInclude Include="Settings\HddCreateQt.h">
@ -337,6 +344,9 @@
<QtMoc Include="Settings\FolderSettingsWidget.h">
<Filter>Settings</Filter>
</QtMoc>
<QtMoc Include="QtHost.h" />
<QtMoc Include="CoverDownloadDialog.h" />
<QtMoc Include="QtProgressCallback.h" />
</ItemGroup>
<ItemGroup>
<QtResource Include="resources\resources.qrc">
@ -427,6 +437,7 @@
<QtUi Include="Settings\ControllerMacroEditWidget.ui">
<Filter>Settings</Filter>
</QtUi>
<QtUi Include="CoverDownloadDialog.ui" />
</ItemGroup>
<ItemGroup>
<None Include="Settings\FolderSettingsWidget.ui">

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M20 5H4v14l9.292-9.294a1 1 0 0 1 1.414 0L20 15.01V5zM2 3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993zM8 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" fill="#000000"/></svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M20 5H4v14l9.292-9.294a1 1 0 0 1 1.414 0L20 15.01V5zM2 3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993zM8 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" fill="#ffffff"/></svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@ -33,6 +33,7 @@
<file>icons/black/svg/gamepad-line.svg</file>
<file>icons/black/svg/global-line.svg</file>
<file>icons/black/svg/hard-drive-2-line.svg</file>
<file>icons/black/svg/image-fill.svg</file>
<file>icons/black/svg/keyboard-line.svg</file>
<file>icons/black/svg/layout-grid-line.svg</file>
<file>icons/black/svg/list-check.svg</file>
@ -87,6 +88,7 @@
<file>icons/white/svg/gamepad-line.svg</file>
<file>icons/white/svg/global-line.svg</file>
<file>icons/white/svg/hard-drive-2-line.svg</file>
<file>icons/white/svg/image-fill.svg</file>
<file>icons/white/svg/keyboard-line.svg</file>
<file>icons/white/svg/layout-grid-line.svg</file>
<file>icons/white/svg/list-check.svg</file>

View File

@ -960,7 +960,6 @@ struct Pcsx2Config
#endif
// when enabled uses BOOT2 injection, skipping sony bios splashes
UseBOOT2Injection : 1,
PatchBios : 1,
BackupSavestate : 1,
SavestateZstdCompression : 1,
// enables simulated ejection of memory cards when loading savestates
@ -995,8 +994,6 @@ struct Pcsx2Config
FilenameOptions BaseFilenames;
std::string PatchRegion;
// Memorycard options - first 2 are default slots, last 6 are multitap 1 and 2
// slots (3 each)
McdOptions Mcd[8];
@ -1029,6 +1026,9 @@ struct Pcsx2Config
// You shouldn't assign to this class, because it'll mess with the runtime variables (Current...).
// But you can still use this to copy config. Only needed until we drop wx.
void CopyConfig(const Pcsx2Config& cfg);
/// Copies runtime configuration settings (e.g. frame limiter state).
void CopyRuntimeConfig(Pcsx2Config& cfg);
};
extern Pcsx2Config EmuConfig;

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,7 @@ namespace FullscreenUI
void Shutdown();
void Render();
void InvalidateCoverCache();
class ProgressCallback final : public BaseProgressCallback
{
@ -42,6 +43,8 @@ namespace FullscreenUI
ProgressCallback(std::string name);
~ProgressCallback() override;
__fi const std::string& GetName() const { return m_name; }
void PushState() override;
void PopState() override;

View File

@ -20,6 +20,7 @@
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/FileSystem.h"
#include "common/HTTPDownloader.h"
#include "common/Path.h"
#include "common/ProgressCallback.h"
#include "common/StringUtil.h"
@ -1013,7 +1014,8 @@ std::string GameList::GetCoverImagePath(const std::string& path, const std::stri
// Last resort, check the game title
if (!title.empty())
{
const std::string cover_filename(title + extension);
std::string cover_filename(title + extension);
Path::SanitizeFileName(&cover_filename);
cover_path = Path::Combine(EmuFolders::Covers, cover_filename);
if (FileSystem::FileExists(cover_path.c_str()))
return cover_path;
@ -1024,7 +1026,7 @@ std::string GameList::GetCoverImagePath(const std::string& path, const std::stri
return cover_path;
}
std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename)
std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename, bool use_serial)
{
const char* extension = std::strrchr(new_filename, '.');
if (!extension)
@ -1038,6 +1040,132 @@ std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const cha
return existing_filename;
}
const std::string cover_filename(entry->title + extension);
std::string cover_filename(use_serial ? (entry->serial + extension) : (entry->title + extension));
Path::SanitizeFileName(&cover_filename);
return Path::Combine(EmuFolders::Covers, cover_filename);
}
bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, bool use_serial, ProgressCallback* progress,
std::function<void(const Entry*, std::string)> save_callback)
{
if (!progress)
progress = ProgressCallback::NullProgressCallback;
bool has_title = false;
bool has_file_title = false;
bool has_serial = false;
for (const std::string& url_template : url_templates)
{
if (!has_title && url_template.find("${title}") != std::string::npos)
has_title = true;
if (!has_file_title && url_template.find("${filetitle}") != std::string::npos)
has_file_title = true;
if (!has_serial && url_template.find("${serial}") != std::string::npos)
has_serial = true;
}
if (!has_title && !has_file_title && !has_serial)
{
progress->DisplayError("URL template must contain at least one of ${title}, ${filetitle}, or ${serial}.");
return false;
}
std::vector<std::pair<std::string, std::string>> download_urls;
{
std::unique_lock lock(s_mutex);
for (const GameList::Entry& entry : m_entries)
{
const std::string existing_path(GetCoverImagePathForEntry(&entry));
if (!existing_path.empty())
continue;
for (const std::string& url_template : url_templates)
{
std::string url(url_template);
if (has_title)
StringUtil::ReplaceAll(&url, "${title}", Common::HTTPDownloader::URLEncode(entry.title));
if (has_file_title)
{
std::string display_name(FileSystem::GetDisplayNameFromPath(entry.path));
StringUtil::ReplaceAll(&url, "${filetitle}", Common::HTTPDownloader::URLEncode(Path::GetFileTitle(display_name)));
}
if (has_serial)
StringUtil::ReplaceAll(&url, "${serial}", Common::HTTPDownloader::URLEncode(entry.serial));
download_urls.emplace_back(entry.path, std::move(url));
}
}
}
if (download_urls.empty())
{
progress->DisplayError("No URLs to download enumerated.");
return false;
}
std::unique_ptr<Common::HTTPDownloader> downloader(Common::HTTPDownloader::Create());
if (!downloader)
{
progress->DisplayError("Failed to create HTTP downloader.");
return false;
}
progress->SetCancellable(true);
progress->SetProgressRange(static_cast<u32>(download_urls.size()));
for (auto& [entry_path, url] : download_urls)
{
if (progress->IsCancelled())
break;
// make sure it didn't get done already
{
std::unique_lock lock(s_mutex);
const GameList::Entry* entry = GetEntryForPath(entry_path.c_str());
if (!entry || !GetCoverImagePathForEntry(entry).empty())
{
progress->IncrementProgressValue();
continue;
}
progress->SetFormattedStatusText("Downloading cover for %s [%s]...", entry->title.c_str(), entry->serial.c_str());
}
// we could actually do a few in parallel here...
std::string filename(Common::HTTPDownloader::URLDecode(url));
downloader->CreateRequest(std::move(url), [use_serial, &save_callback, entry_path = std::move(entry_path),
filename = std::move(filename)](s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data) {
if (status_code != Common::HTTPDownloader::HTTP_OK || data.empty())
return;
std::unique_lock lock(s_mutex);
const GameList::Entry* entry = GetEntryForPath(entry_path.c_str());
if (!entry || !GetCoverImagePathForEntry(entry).empty())
return;
// prefer the content type from the response for the extension
// otherwise, if it's missing, and the request didn't have an extension.. fall back to jpegs.
std::string template_filename;
std::string content_type_extension(Common::HTTPDownloader::GetExtensionForContentType(content_type));
// don't treat the domain name as an extension..
const std::string::size_type last_slash = filename.find('/');
const std::string::size_type last_dot = filename.find('.');
if (!content_type_extension.empty())
template_filename = fmt::format("cover.{}", content_type_extension);
else if (last_slash != std::string::npos && last_dot != std::string::npos && last_dot > last_slash)
template_filename = Path::GetFileName(filename);
else
template_filename = "cover.jpg";
std::string write_path(GetNewCoverImagePathForEntry(entry, template_filename.c_str(), use_serial));
if (write_path.empty())
return;
if (FileSystem::WriteBinaryFile(write_path.c_str(), data.data(), data.size()) && save_callback)
save_callback(entry, std::move(write_path));
});
downloader->WaitForAllRequests();
progress->IncrementProgressValue();
}
return true;
}

View File

@ -17,6 +17,7 @@
#include "GameDatabase.h"
#include "common/Pcsx2Defs.h"
#include <ctime>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
@ -125,5 +126,10 @@ namespace GameList
std::string GetCoverImagePathForEntry(const Entry* entry);
std::string GetCoverImagePath(const std::string& path, const std::string& code, const std::string& title);
std::string GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename);
std::string GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename, bool use_serial = false);
/// Downloads covers using the specified URL templates. By default, covers are saved by title, but this can be changed with
/// the use_serial parameter. save_callback optionall takes the entry and the path the new cover is saved to.
bool DownloadCovers(const std::vector<std::string>& url_templates, bool use_serial = false, ProgressCallback* progress = nullptr,
std::function<void(const Entry*, std::string)> save_callback = {});
} // namespace GameList

View File

@ -454,6 +454,17 @@ void ImGuiFullscreen::BeginLayout()
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(8.0f, 8.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(4.0f, 3.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(8.0f, 4.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, LayoutScale(4.0f, 4.0f));
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, LayoutScale(4.0f, 2.0f));
ImGui::PushStyleVar(ImGuiStyleVar_IndentSpacing, LayoutScale(21.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, LayoutScale(14.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_GrabMinSize, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_TabRounding, LayoutScale(4.0f));
ImGui::PushStyleColor(ImGuiCol_Text, UISecondaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TextDisabled, UIDisabledColor);
ImGui::PushStyleColor(ImGuiCol_Button, UISecondaryColor);
@ -483,7 +494,7 @@ void ImGuiFullscreen::EndLayout()
DrawToast();
ImGui::PopStyleColor(10);
ImGui::PopStyleVar(2);
ImGui::PopStyleVar(12);
}
void ImGuiFullscreen::QueueResetFocus()
@ -559,10 +570,11 @@ void ImGuiFullscreen::PopSecondaryColor()
ImGui::PopStyleColor(5);
}
bool ImGuiFullscreen::BeginFullscreenColumns(const char* title)
bool ImGuiFullscreen::BeginFullscreenColumns(const char* title, float pos_y, bool expand_to_screen_width)
{
ImGui::SetNextWindowPos(ImVec2(g_layout_padding_left, 0.0f));
ImGui::SetNextWindowSize(ImVec2(LayoutScale(LAYOUT_SCREEN_WIDTH), ImGui::GetIO().DisplaySize.y));
ImGui::SetNextWindowPos(ImVec2(expand_to_screen_width ? 0.0f : g_layout_padding_left, pos_y));
ImGui::SetNextWindowSize(ImVec2(
expand_to_screen_width ? ImGui::GetIO().DisplaySize.x : LayoutScale(LAYOUT_SCREEN_WIDTH), ImGui::GetIO().DisplaySize.y - pos_y));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
@ -592,8 +604,16 @@ void ImGuiFullscreen::EndFullscreenColumns()
bool ImGuiFullscreen::BeginFullscreenColumnWindow(float start, float end, const char* name, const ImVec4& background)
{
const ImVec2 pos(LayoutScale(start), 0.0f);
const ImVec2 size(LayoutScale(end - start), ImGui::GetIO().DisplaySize.y);
start = LayoutScale(start);
end = LayoutScale(end);
if (start < 0.0f)
start = ImGui::GetIO().DisplaySize.x + start;
if (end <= 0.0f)
end = ImGui::GetIO().DisplaySize.x + end;
const ImVec2 pos(start, 0.0f);
const ImVec2 size(end - start, ImGui::GetCurrentWindow()->Size.y);
ImGui::PushStyleColor(ImGuiCol_ChildBg, background);
@ -905,6 +925,32 @@ bool ImGuiFullscreen::MenuButton(const char* title, const char* summary, bool en
return pressed;
}
bool ImGuiFullscreen::MenuButtonWithoutSummary(const char* title, bool enabled, float height, ImFont* font, const ImVec2& text_align)
{
ImRect bb;
bool visible, hovered;
bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb);
if (!visible)
return false;
const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f);
const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint));
const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max);
if (!enabled)
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
ImGui::PushFont(font);
ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, text_align, &title_bb);
ImGui::PopFont();
if (!enabled)
ImGui::PopStyleColor();
s_menu_button_index++;
return pressed;
}
bool ImGuiFullscreen::MenuImageButton(const char* title, const char* summary, ImTextureID user_texture_id, const ImVec2& image_size,
bool enabled, float height, const ImVec2& uv0, const ImVec2& uv1, ImFont* title_font, ImFont* summary_font)
{
@ -1218,6 +1264,7 @@ bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, s32* v
bool changed = false;
ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f));
ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::PushFont(g_large_font);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
@ -1286,6 +1333,7 @@ bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, float*
bool changed = false;
ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f));
ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::PushFont(g_large_font);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
@ -1520,7 +1568,7 @@ void ImGuiFullscreen::PopulateFileSelectorItems()
for (std::string& root_path : FileSystem::GetRootDirectoryList())
{
s_file_selector_items.emplace_back(
StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", root_path.c_str()), std::move(root_path), false);
StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", root_path.c_str()), std::move(root_path), false);
}
}
else
@ -1537,7 +1585,7 @@ void ImGuiFullscreen::PopulateFileSelectorItems()
//FIXME FileSystem::CanonicalizePath(parent_path, true);
}
s_file_selector_items.emplace_back(ICON_FA_FOLDER_OPEN " <Parent Directory>", std::move(parent_path), false);
s_file_selector_items.emplace_back(ICON_FA_FOLDER_OPEN " <Parent Directory>", std::move(parent_path), false);
std::sort(results.begin(), results.end(), [](const FILESYSTEM_FIND_DATA& lhs, const FILESYSTEM_FIND_DATA& rhs) {
if ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != (rhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY))
return (lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != 0;
@ -1554,7 +1602,7 @@ void ImGuiFullscreen::PopulateFileSelectorItems()
if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)
{
std::string title(StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", fd.FileName.c_str()));
std::string title(StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", fd.FileName.c_str()));
s_file_selector_items.emplace_back(std::move(title), std::move(full_path), false);
}
else
@ -1566,7 +1614,7 @@ void ImGuiFullscreen::PopulateFileSelectorItems()
continue;
}
std::string title(StringUtil::StdStringFromFormat(ICON_FA_FILE " %s", fd.FileName.c_str()));
std::string title(StringUtil::StdStringFromFormat(ICON_FA_FILE " %s", fd.FileName.c_str()));
s_file_selector_items.emplace_back(std::move(title), std::move(full_path), true);
}
}
@ -1637,7 +1685,7 @@ void ImGuiFullscreen::DrawFileSelector()
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, MulAlpha(UIBackgroundColor, 0.95f));
bool is_open = !WantsToCloseMenu();
bool directory_selected = false;
@ -1650,13 +1698,13 @@ void ImGuiFullscreen::DrawFileSelector()
if (!s_file_selector_current_directory.empty())
{
MenuButton(fmt::format(ICON_FA_FOLDER_OPEN " {}", s_file_selector_current_directory).c_str(), nullptr, false,
MenuButton(fmt::format(ICON_FA_FOLDER_OPEN " {}", s_file_selector_current_directory).c_str(), nullptr, false,
LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
}
if (s_file_selector_directory && !s_file_selector_current_directory.empty())
{
if (MenuButton(ICON_FA_FOLDER_PLUS " <Use This Directory>", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
if (MenuButton(ICON_FA_FOLDER_PLUS " <Use This Directory>", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
directory_selected = true;
}
@ -1739,21 +1787,22 @@ void ImGuiFullscreen::DrawChoiceDialog()
if (!s_choice_dialog_open)
return;
const float width = 600.0f;
const float title_height = g_large_font->FontSize + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().WindowPadding.y * 2.0f;
const float height = std::min(400.0f, title_height + (LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + (LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f)) *
static_cast<float>(s_choice_dialog_options.size()));
ImGui::SetNextWindowSize(LayoutScale(width, height));
ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::OpenPopup(s_choice_dialog_title.c_str());
ImGui::PushFont(g_large_font);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, MulAlpha(UIBackgroundColor, 0.95f));
const float width = LayoutScale(600.0f);
const float title_height = g_large_font->FontSize + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().WindowPadding.y * 2.0f;
const float height = std::min(
LayoutScale(400.0f), title_height + LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + (LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f)) *
static_cast<float>(s_choice_dialog_options.size()));
ImGui::SetNextWindowSize(ImVec2(width, height));
ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::OpenPopup(s_choice_dialog_title.c_str());
bool is_open = !WantsToCloseMenu();
s32 choice = -1;
@ -1771,7 +1820,7 @@ void ImGuiFullscreen::DrawChoiceDialog()
{
auto& option = s_choice_dialog_options[i];
const std::string title(fmt::format("{0} {1}", option.second ? ICON_FA_CHECK_SQUARE : ICON_FA_SQUARE, option.first));
const std::string title(fmt::format("{0} {1}", option.second ? ICON_FA_CHECK_SQUARE : ICON_FA_SQUARE, option.first));
if (MenuButton(title.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
{
choice = i;
@ -1786,7 +1835,7 @@ void ImGuiFullscreen::DrawChoiceDialog()
auto& option = s_choice_dialog_options[i];
std::string title;
if (option.second)
title += ICON_FA_CHECK " ";
title += ICON_FA_CHECK " ";
title += option.first;
if (ActiveButton(title.c_str(), option.second, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
@ -1858,23 +1907,32 @@ void ImGuiFullscreen::DrawInputDialog()
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, MulAlpha(UIBackgroundColor, 0.95f));
bool is_open = true;
if (ImGui::BeginPopupModal(s_input_dialog_title.c_str(), &is_open,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
{
ImGui::TextWrapped("%s", s_input_dialog_message.c_str());
ImGui::NewLine();
if (!s_input_dialog_caption.empty())
ImGui::TextUnformatted(s_input_dialog_caption.c_str());
ImGui::InputText("##input", &s_input_dialog_text);
ImGui::NewLine();
BeginMenuButtons();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
if (!s_input_dialog_caption.empty())
{
const float prev = ImGui::GetCursorPosX();
ImGui::TextUnformatted(s_input_dialog_caption.c_str());
ImGui::SetNextItemWidth(ImGui::GetCursorPosX() - prev);
}
else
{
ImGui::SetNextItemWidth(ImGui::GetCurrentWindow()->WorkRect.GetWidth());
}
ImGui::InputText("##input", &s_input_dialog_text);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
const bool ok_enabled = !s_input_dialog_text.empty();
if (ActiveButton(s_input_dialog_ok_text.c_str(), false, ok_enabled) && ok_enabled)
@ -1887,7 +1945,7 @@ void ImGuiFullscreen::DrawInputDialog()
cb(std::move(text));
}
if (ActiveButton(ICON_FA_TIMES " Cancel", false))
if (ActiveButton(ICON_FA_TIMES " Cancel", false))
{
CloseInputDialog();
@ -1994,7 +2052,7 @@ void ImGuiFullscreen::DrawMessageDialog()
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, MulAlpha(UIBackgroundColor, 0.95f));
bool is_open = true;
const u32 flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
@ -2006,7 +2064,7 @@ void ImGuiFullscreen::DrawMessageDialog()
BeginMenuButtons();
ImGui::TextWrapped("%s", s_message_dialog_message.c_str());
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(20.0f));
for (s32 button_index = 0; button_index < static_cast<s32>(s_message_dialog_buttons.size()); button_index++)
{
@ -2033,9 +2091,17 @@ void ImGuiFullscreen::DrawMessageDialog()
CloseMessageDialog();
if (std::holds_alternative<InfoMessageDialogCallback>(cb))
std::get<InfoMessageDialogCallback>(cb)();
{
const InfoMessageDialogCallback& func = std::get<InfoMessageDialogCallback>(cb);
if (func)
func();
}
else if (std::holds_alternative<ConfirmMessageDialogCallback>(cb))
std::get<ConfirmMessageDialogCallback>(cb)(result.value_or(1) == 0);
{
const ConfirmMessageDialogCallback& func = std::get<ConfirmMessageDialogCallback>(cb);
if (func)
func(result.value_or(1) == 0);
}
}
}
@ -2362,41 +2428,44 @@ void ImGuiFullscreen::DrawToast()
}
}
void ImGuiFullscreen::SetTheme()
void ImGuiFullscreen::SetTheme(bool light)
{
#if 1
// dark
UIBackgroundColor = HEX_TO_IMVEC4(0x212121, 0xff);
UIBackgroundTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
UIBackgroundLineColor = HEX_TO_IMVEC4(0xf0f0f0, 0xff);
UIBackgroundHighlightColor = HEX_TO_IMVEC4(0x4b4b4b, 0xff);
UIPrimaryColor = HEX_TO_IMVEC4(0x2e2e2e, 0xff);
UIPrimaryLightColor = HEX_TO_IMVEC4(0x484848, 0xff);
UIPrimaryDarkColor = HEX_TO_IMVEC4(0x000000, 0xff);
UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff);
UITextHighlightColor = HEX_TO_IMVEC4(0x90caf9, 0xff);
UIPrimaryLineColor = HEX_TO_IMVEC4(0xffffff, 0xff);
UISecondaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff);
UISecondaryLightColor = HEX_TO_IMVEC4(0x63a4ff, 0xff);
UISecondaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff);
UISecondaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
#elif 1
// light
UIBackgroundColor = HEX_TO_IMVEC4(0xf5f5f6, 0xff);
UIBackgroundTextColor = HEX_TO_IMVEC4(0x000000, 0xff);
UIBackgroundLineColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff);
UIBackgroundHighlightColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff);
UIPrimaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff);
UIPrimaryLightColor = HEX_TO_IMVEC4(0x5472d3, 0xff);
UIPrimaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff);
UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff);
UITextHighlightColor = HEX_TO_IMVEC4(0x8e8e8e, 0xff);
UIPrimaryLineColor = HEX_TO_IMVEC4(0x000000, 0xff);
UISecondaryColor = HEX_TO_IMVEC4(0x3d5afe, 0xff);
UISecondaryLightColor = HEX_TO_IMVEC4(0xc0cfff, 0xff);
UISecondaryDarkColor = HEX_TO_IMVEC4(0x0031ca, 0xff);
UISecondaryTextColor = HEX_TO_IMVEC4(0x000000, 0xff);
#endif
if (!light)
{
// dark
UIBackgroundColor = HEX_TO_IMVEC4(0x212121, 0xff);
UIBackgroundTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
UIBackgroundLineColor = HEX_TO_IMVEC4(0xf0f0f0, 0xff);
UIBackgroundHighlightColor = HEX_TO_IMVEC4(0x4b4b4b, 0xff);
UIPrimaryColor = HEX_TO_IMVEC4(0x2e2e2e, 0xff);
UIPrimaryLightColor = HEX_TO_IMVEC4(0x484848, 0xff);
UIPrimaryDarkColor = HEX_TO_IMVEC4(0x000000, 0xff);
UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff);
UITextHighlightColor = HEX_TO_IMVEC4(0x90caf9, 0xff);
UIPrimaryLineColor = HEX_TO_IMVEC4(0xffffff, 0xff);
UISecondaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff);
UISecondaryLightColor = HEX_TO_IMVEC4(0x63a4ff, 0xff);
UISecondaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff);
UISecondaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
}
else
{
// light
UIBackgroundColor = HEX_TO_IMVEC4(0xf5f5f6, 0xff);
UIBackgroundTextColor = HEX_TO_IMVEC4(0x000000, 0xff);
UIBackgroundLineColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff);
UIBackgroundHighlightColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff);
UIPrimaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff);
UIPrimaryLightColor = HEX_TO_IMVEC4(0x5472d3, 0xff);
UIPrimaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff);
UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff);
UITextHighlightColor = HEX_TO_IMVEC4(0x8e8e8e, 0xff);
UIPrimaryLineColor = HEX_TO_IMVEC4(0x000000, 0xff);
UISecondaryColor = HEX_TO_IMVEC4(0x3d5afe, 0xff);
UISecondaryLightColor = HEX_TO_IMVEC4(0xc0cfff, 0xff);
UISecondaryDarkColor = HEX_TO_IMVEC4(0x0031ca, 0xff);
UISecondaryTextColor = HEX_TO_IMVEC4(0x000000, 0xff);
}
}

View File

@ -92,6 +92,7 @@ namespace ImGuiFullscreen
}
static __fi ImVec4 ModAlpha(const ImVec4& v, float a) { return ImVec4(v.x, v.y, v.z, a); }
static __fi ImVec4 MulAlpha(const ImVec4& v, float a) { return ImVec4(v.x, v.y, v.z, v.w * a); }
/// Centers an image within the specified bounds, scaling up or down as needed.
ImRect CenterImage(const ImVec2& fit_size, const ImVec2& image_size);
@ -100,7 +101,7 @@ namespace ImGuiFullscreen
/// Initializes, setting up any state.
bool Initialize(const char* placeholder_image_path);
void SetTheme();
void SetTheme(bool light);
void SetFonts(ImFont* standard_font, ImFont* medium_font, ImFont* large_font);
bool UpdateLayoutScale();
@ -130,7 +131,7 @@ namespace ImGuiFullscreen
void DrawWindowTitle(const char* title);
bool BeginFullscreenColumns(const char* title = nullptr);
bool BeginFullscreenColumns(const char* title = nullptr, float pos_y = 0.0f, bool expand_to_screen_width = false);
void EndFullscreenColumns();
bool BeginFullscreenColumnWindow(float start, float end, const char* name, const ImVec4& background = UIBackgroundColor);
@ -153,6 +154,8 @@ namespace ImGuiFullscreen
ImFont* font = g_large_font);
bool MenuButton(const char* title, const char* summary, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT,
ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
bool MenuButtonWithoutSummary(const char* title, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY,
ImFont* font = g_large_font, const ImVec2& text_align = ImVec2(0.0f, 0.0f));
bool MenuButtonWithValue(const char* title, const char* summary, const char* value, bool enabled = true,
float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
bool MenuImageButton(const char* title, const char* summary, ImTextureID user_texture_id, const ImVec2& image_size, bool enabled = true,
@ -226,9 +229,9 @@ namespace ImGuiFullscreen
using MessageDialogCallback = std::function<void(s32)>;
bool IsMessageBoxDialogOpen();
void OpenConfirmMessageDialog(std::string title, std::string message, ConfirmMessageDialogCallback callback,
std::string yes_button_text = ICON_FA_CHECK " Yes", std::string no_button_text = ICON_FA_TIMES " No");
void OpenInfoMessageDialog(std::string title, std::string message, InfoMessageDialogCallback callback,
std::string button_text = ICON_FA_WINDOW_CLOSE " Close");
std::string yes_button_text = ICON_FA_CHECK " Yes", std::string no_button_text = ICON_FA_TIMES " No");
void OpenInfoMessageDialog(std::string title, std::string message, InfoMessageDialogCallback callback = {},
std::string button_text = ICON_FA_WINDOW_CLOSE " Close");
void OpenMessageDialog(std::string title, std::string message, MessageDialogCallback callback, std::string first_button_text,
std::string second_button_text, std::string third_button_text);
void CloseMessageDialog();

View File

@ -82,6 +82,7 @@ static Common::Timer s_last_render_time;
// cached copies of WantCaptureKeyboard/Mouse, used to know when to dispatch events
static std::atomic_bool s_imgui_wants_keyboard{false};
static std::atomic_bool s_imgui_wants_mouse{false};
static std::atomic_bool s_imgui_wants_text{false};
// mapping of host key -> imgui key
static std::unordered_map<u32, ImGuiKey> s_imgui_key_map;
@ -197,11 +198,7 @@ void ImGuiManager::UpdateScale()
ImGui::EndFrame();
s_global_scale = scale;
ImGui::GetStyle() = ImGuiStyle();
ImGui::GetStyle().WindowMinSize = ImVec2(1.0f, 1.0f);
SetStyle();
ImGui::GetStyle().ScaleAllSizes(scale);
if (!AddImGuiFonts(HasFullscreenFonts()))
pxFailRel("Failed to create ImGui font text");
@ -225,6 +222,16 @@ void ImGuiManager::NewFrame()
ImGui::GetCurrentWindowRead()->Flags |= ImGuiWindowFlags_NoNavInputs;
s_imgui_wants_keyboard.store(io.WantCaptureKeyboard, std::memory_order_relaxed);
s_imgui_wants_mouse.store(io.WantCaptureMouse, std::memory_order_release);
const bool want_text_input = io.WantTextInput;
if (s_imgui_wants_text.load(std::memory_order_relaxed) != want_text_input)
{
s_imgui_wants_text.store(want_text_input, std::memory_order_release);
if (want_text_input)
Host::BeginTextInput();
else
Host::EndTextInput();
}
#endif
}
@ -411,13 +418,13 @@ ImFont* ImGuiManager::AddFixedFont(float size)
bool ImGuiManager::AddIconFonts(float size)
{
static constexpr ImWchar range_fa[] = { 0xf001,0xf002,0xf005,0xf005,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf021,0xf021,0xf025,0xf025,0xf028,0xf028,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03d,0xf04a,0xf04c,0xf04e,0xf04e,0xf050,0xf050,0xf052,0xf052,0xf059,0xf059,0xf05e,0xf05e,0xf065,0xf065,0xf067,0xf067,0xf06a,0xf06a,0xf071,0xf071,0xf07b,0xf07c,0xf085,0xf085,0xf091,0xf091,0xf0a0,0xf0a0,0xf0ac,0xf0ad,0xf0b0,0xf0b0,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0d0,0xf0d0,0xf0e2,0xf0e2,0xf0eb,0xf0eb,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf11b,0xf11c,0xf121,0xf121,0xf140,0xf140,0xf144,0xf144,0xf14a,0xf14a,0xf15b,0xf15b,0xf188,0xf188,0xf192,0xf192,0xf1c9,0xf1c9,0xf1dd,0xf1de,0xf1e6,0xf1e6,0xf1ea,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f5,0xf2f5,0xf302,0xf302,0xf3fd,0xf3fd,0xf410,0xf410,0xf466,0xf466,0xf479,0xf479,0xf517,0xf517,0xf51f,0xf51f,0xf543,0xf543,0xf545,0xf545,0xf547,0xf548,0xf552,0xf552,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf756,0xf756,0xf7c2,0xf7c2,0xf815,0xf815,0xf818,0xf818,0xf8cc,0xf8cc,0x0,0x0 };
static constexpr ImWchar range_fa[] = { 0xf001,0xf002,0xf005,0xf005,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf021,0xf021,0xf025,0xf025,0xf028,0xf028,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03d,0xf04a,0xf04c,0xf04e,0xf04e,0xf050,0xf050,0xf052,0xf052,0xf059,0xf059,0xf05e,0xf05e,0xf065,0xf065,0xf067,0xf067,0xf06a,0xf06a,0xf071,0xf071,0xf07b,0xf07c,0xf085,0xf085,0xf091,0xf091,0xf0a0,0xf0a0,0xf0ac,0xf0ad,0xf0b0,0xf0b0,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0d0,0xf0d0,0xf0e2,0xf0e2,0xf0eb,0xf0eb,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf119,0xf119,0xf11b,0xf11c,0xf121,0xf121,0xf133,0xf133,0xf140,0xf140,0xf144,0xf144,0xf14a,0xf14a,0xf15b,0xf15b,0xf188,0xf188,0xf192,0xf192,0xf1c9,0xf1c9,0xf1dd,0xf1de,0xf1e6,0xf1e6,0xf1ea,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f5,0xf2f5,0xf302,0xf302,0xf3fd,0xf3fd,0xf410,0xf410,0xf466,0xf466,0xf479,0xf479,0xf517,0xf517,0xf51f,0xf51f,0xf543,0xf543,0xf545,0xf545,0xf547,0xf548,0xf552,0xf552,0xf5aa,0xf5aa,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf756,0xf756,0xf7c2,0xf7c2,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf8cc,0xf8cc,0x0,0x0 };
ImFontConfig cfg;
cfg.MergeMode = true;
cfg.PixelSnapH = true;
cfg.GlyphMinAdvanceX = size * 0.75f;
cfg.GlyphMaxAdvanceX = size * 0.75f;
cfg.GlyphMinAdvanceX = size;
cfg.GlyphMaxAdvanceX = size;
cfg.FontDataOwnedByAtlas = false;
return (ImGui::GetIO().Fonts->AddFontFromMemoryTTF(
@ -875,12 +882,12 @@ ImFont* ImGuiManager::GetLargeFont()
bool ImGuiManager::WantsTextInput()
{
return s_imgui_wants_keyboard.load(std::memory_order_acquire);
return s_imgui_wants_text.load(std::memory_order_acquire);
}
void ImGuiManager::AddTextInput(std::string str)
{
if (!s_imgui_wants_keyboard.load(std::memory_order_acquire))
if (!s_imgui_wants_text.load(std::memory_order_acquire))
return;
// Has to go through the CPU -> GS thread :(

View File

@ -90,3 +90,12 @@ namespace ImGuiManager
#endif
} // namespace ImGuiManager
namespace Host
{
/// Called by ImGuiManager when the cursor enters a text field. The host may choose to open an on-screen
/// keyboard for devices without a physical keyboard.
void BeginTextInput();
/// Called by ImGuiManager when the cursor leaves a text field.
void EndTextInput();
}

View File

@ -99,6 +99,7 @@ namespace InputManager
static std::vector<std::string_view> SplitChord(const std::string_view& binding);
static bool SplitBinding(const std::string_view& binding, std::string_view* source, std::string_view* sub_binding);
static void AddBinding(const std::string_view& binding, const InputEventHandler& handler);
static void AddBindings(const std::vector<std::string>& bindings, const InputEventHandler& handler);
static bool ParseBindingAndGetSource(const std::string_view& binding, InputBindingKey* key, InputSource** source);
@ -304,48 +305,51 @@ std::string InputManager::ConvertInputBindingKeysToString(const InputBindingKey*
return ss.str();
}
void InputManager::AddBindings(const std::vector<std::string>& bindings, const InputEventHandler& handler)
void InputManager::AddBinding(const std::string_view& binding, const InputEventHandler& handler)
{
for (const std::string& binding : bindings)
std::shared_ptr<InputBinding> ibinding;
const std::vector<std::string_view> chord_bindings(SplitChord(binding));
for (const std::string_view& chord_binding : chord_bindings)
{
std::shared_ptr<InputBinding> ibinding;
const std::vector<std::string_view> chord_bindings(SplitChord(binding));
for (const std::string_view& chord_binding : chord_bindings)
std::optional<InputBindingKey> key = ParseInputBindingKey(chord_binding);
if (!key.has_value())
{
std::optional<InputBindingKey> key = ParseInputBindingKey(chord_binding);
if (!key.has_value())
{
Console.WriteLn("Invalid binding: '%s'", binding.c_str());
ibinding.reset();
break;
}
if (!ibinding)
{
ibinding = std::make_shared<InputBinding>();
ibinding->handler = handler;
}
if (ibinding->num_keys == MAX_KEYS_PER_BINDING)
{
Console.WriteLn("Too many chord parts, max is %u (%s)", MAX_KEYS_PER_BINDING, binding.c_str());
ibinding.reset();
break;
}
ibinding->keys[ibinding->num_keys] = key.value();
ibinding->full_mask |= (static_cast<u8>(1) << ibinding->num_keys);
ibinding->num_keys++;
Console.WriteLn(fmt::format("Invalid binding: '{}'", binding));
ibinding.reset();
break;
}
if (!ibinding)
continue;
{
ibinding = std::make_shared<InputBinding>();
ibinding->handler = handler;
}
// plop it in the input map for all the keys
for (u32 i = 0; i < ibinding->num_keys; i++)
s_binding_map.emplace(ibinding->keys[i].MaskDirection(), ibinding);
if (ibinding->num_keys == MAX_KEYS_PER_BINDING)
{
Console.WriteLn(fmt::format("Too many chord parts, max is {} ({})", MAX_KEYS_PER_BINDING, binding));
ibinding.reset();
break;
}
ibinding->keys[ibinding->num_keys] = key.value();
ibinding->full_mask |= (static_cast<u8>(1) << ibinding->num_keys);
ibinding->num_keys++;
}
if (!ibinding)
return;
// plop it in the input map for all the keys
for (u32 i = 0; i < ibinding->num_keys; i++)
s_binding_map.emplace(ibinding->keys[i].MaskDirection(), ibinding);
}
void InputManager::AddBindings(const std::vector<std::string>& bindings, const InputEventHandler& handler)
{
for (const std::string& binding : bindings)
AddBinding(binding, handler);
}
// ------------------------------------------------------------------------
@ -559,7 +563,7 @@ void InputManager::AddPadBindings(SettingsInterface& si, u32 pad_index, const ch
if (!bindings.empty())
{
// we use axes for all pad bindings to simplify things, and because they are pressure sensitive
AddBindings(bindings, InputAxisEventHandler{[pad_index, bind_index, bind_names](
AddBindings(bindings, InputAxisEventHandler{[pad_index, bind_index](
float value) { PAD::SetControllerState(pad_index, bind_index, value); }});
}
}
@ -1006,10 +1010,8 @@ void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& bind
{
// From lilypad: 1 mouse pixel = 1/8th way down.
const float default_scale = (axis <= static_cast<u32>(InputPointerAxis::Y)) ? 8.0f : 1.0f;
const float invert =
si.GetBoolValue("Pad", fmt::format("Pointer{}Invert", s_pointer_axis_names[axis]).c_str(), false) ? -1.0f : 1.0f;
s_pointer_axis_scale[axis] =
invert /
1.0f /
std::max(si.GetFloatValue("Pad", fmt::format("Pointer{}Scale", s_pointer_axis_names[axis]).c_str(), default_scale), 1.0f);
}
}

View File

@ -766,14 +766,14 @@ void GSRenderer::QueueSnapshot(const std::string& path, u32 gsdump_frames)
// append the game serial and title
if (std::string name(GetDumpName()); !name.empty())
{
Path::SanitizeFileName(name);
Path::SanitizeFileName(&name);
if (name.length() > 219)
name.resize(219);
m_snapshot += name;
}
if (std::string serial(GetDumpSerial()); !serial.empty())
{
Path::SanitizeFileName(serial);
Path::SanitizeFileName(&serial);
m_snapshot += '_';
m_snapshot += serial;
}

View File

@ -24,7 +24,7 @@ using namespace PAD;
KeyStatus::KeyStatus()
{
Init();
std::memset(&m_analog, 0, sizeof(m_analog));
for (u32 pad = 0; pad < NUM_CONTROLLER_PORTS; pad++)
{
@ -32,6 +32,8 @@ KeyStatus::KeyStatus()
m_axis_scale[pad][1] = 1.0f;
m_pressure_modifier[pad] = 0.5f;
}
Init();
}
void KeyStatus::Init()
@ -70,22 +72,22 @@ void KeyStatus::Set(u32 pad, u32 index, float value)
{
case PAD_R_LEFT:
case PAD_R_RIGHT:
m_analog[pad].rx = MERGE(pad, PAD_R_RIGHT, PAD_R_LEFT);
m_analog[pad].rx = m_analog[pad].invert_rx ? MERGE(pad, PAD_R_LEFT, PAD_R_RIGHT) : MERGE(pad, PAD_R_RIGHT, PAD_R_LEFT);
break;
case PAD_R_DOWN:
case PAD_R_UP:
m_analog[pad].ry = MERGE(pad, PAD_R_DOWN, PAD_R_UP);
m_analog[pad].ry = m_analog[pad].invert_ry ? MERGE(pad, PAD_R_UP, PAD_R_DOWN) : MERGE(pad, PAD_R_DOWN, PAD_R_UP);
break;
case PAD_L_LEFT:
case PAD_L_RIGHT:
m_analog[pad].lx = MERGE(pad, PAD_L_RIGHT, PAD_L_LEFT);
m_analog[pad].lx = m_analog[pad].invert_lx ? MERGE(pad, PAD_L_LEFT, PAD_L_RIGHT) : MERGE(pad, PAD_L_RIGHT, PAD_L_LEFT);
break;
case PAD_L_DOWN:
case PAD_L_UP:
m_analog[pad].ly = MERGE(pad, PAD_L_DOWN, PAD_L_UP);
m_analog[pad].ly = m_analog[pad].invert_ly ? MERGE(pad, PAD_L_UP, PAD_L_DOWN) : MERGE(pad, PAD_L_DOWN, PAD_L_UP);
break;
default:
@ -98,7 +100,8 @@ void KeyStatus::Set(u32 pad, u32 index, float value)
{
// Don't affect L2/R2, since they are analog on most pads.
const float pmod = ((m_button[pad] & (1u << PAD_PRESSURE)) == 0 && !IsTriggerKey(index)) ? m_pressure_modifier[pad] : 1.0f;
m_button_pressure[pad][index] = static_cast<u8>(std::clamp(value * pmod * 255.0f, 0.0f, 255.0f));
const float dz_value = (value < m_button_deadzone[pad]) ? 0.0f : value;
m_button_pressure[pad][index] = static_cast<u8>(std::clamp(dz_value * pmod * 255.0f, 0.0f, 255.0f));
// Since we reordered the buttons for better UI, we need to remap them here.
static constexpr std::array<u8, MAX_KEYS> bitmask_mapping = {{
@ -123,8 +126,7 @@ void KeyStatus::Set(u32 pad, u32 index, float value)
// remainder are analogs and not used here
}};
// TODO: Deadzone here?
if (value > 0.0f)
if (dz_value > 0.0f)
m_button[pad] &= ~(1u << bitmask_mapping[index]);
else
m_button[pad] |= (1u << bitmask_mapping[index]);

View File

@ -30,6 +30,8 @@ namespace PAD
{
u8 lx, ly;
u8 rx, ry;
u8 invert_lx, invert_ly;
u8 invert_rx, invert_ry;
};
PAD::ControllerType m_type[NUM_CONTROLLER_PORTS] = {};
@ -39,6 +41,7 @@ namespace PAD
float m_axis_scale[NUM_CONTROLLER_PORTS][2];
float m_vibration_scale[NUM_CONTROLLER_PORTS][2];
float m_pressure_modifier[NUM_CONTROLLER_PORTS];
float m_button_deadzone[NUM_CONTROLLER_PORTS];
public:
KeyStatus();
@ -58,6 +61,17 @@ namespace PAD
__fi void SetVibrationScale(u32 pad, u32 motor, float scale) { m_vibration_scale[pad][motor] = scale; }
__fi float GetPressureModifier(u32 pad) const { return m_pressure_modifier[pad]; }
__fi void SetPressureModifier(u32 pad, float mod) { m_pressure_modifier[pad] = mod; }
__fi void SetButtonDeadzone(u32 pad, float deadzone) { m_button_deadzone[pad] = deadzone; }
__fi void SetAnalogInvertL(u32 pad, bool x, bool y)
{
m_analog[pad].invert_lx = x;
m_analog[pad].invert_ly = y;
}
__fi void SetAnalogInvertR(u32 pad, bool x, bool y)
{
m_analog[pad].invert_rx = x;
m_analog[pad].invert_ry = y;
}
u32 GetButtons(u32 pad);
u8 GetPressure(u32 pad, u32 index);

View File

@ -268,7 +268,9 @@ void PAD::LoadConfig(const SettingsInterface& si)
const float axis_deadzone = si.GetFloatValue(section.c_str(), "Deadzone", DEFAULT_STICK_DEADZONE);
const float axis_scale = si.GetFloatValue(section.c_str(), "AxisScale", DEFAULT_STICK_SCALE);
const float button_deadzone = si.GetFloatValue(section.c_str(), "ButtonDeadzone", DEFAULT_BUTTON_DEADZONE);
g_key_status.SetAxisScale(i, axis_deadzone, axis_scale);
g_key_status.SetButtonDeadzone(i, button_deadzone);
if (ci->vibration_caps != VibrationCapabilities::NoVibration)
{
@ -281,6 +283,11 @@ void PAD::LoadConfig(const SettingsInterface& si)
const float pressure_modifier = si.GetFloatValue(section.c_str(), "PressureModifier", 1.0f);
g_key_status.SetPressureModifier(i, pressure_modifier);
const int invert_l = si.GetIntValue(section.c_str(), "InvertL", 0);
const int invert_r = si.GetIntValue(section.c_str(), "InvertR", 0);
g_key_status.SetAnalogInvertL(i, (invert_l & 1) != 0, (invert_l & 2) != 0);
g_key_status.SetAnalogInvertR(i, (invert_r & 1) != 0, (invert_r & 2) != 0);
LoadMacroButtonConfig(si, i, type, section);
}
}
@ -310,20 +317,42 @@ void PAD::SetDefaultControllerConfig(SettingsInterface& si)
si.SetBoolValue("Pad", "MultitapPort2", false);
si.SetFloatValue("Pad", "PointerXScale", 8.0f);
si.SetFloatValue("Pad", "PointerYScale", 8.0f);
si.SetBoolValue("Pad", "PointerXInvert", false);
si.SetBoolValue("Pad", "PointerYInvert", false);
// PCSX2 Controller Settings - Default pad types and parameters.
for (u32 i = 0; i < NUM_CONTROLLER_PORTS; i++)
{
const char* type = GetDefaultPadType(i);
const std::string section(GetConfigSection(i));
si.ClearSection(section.c_str());
si.SetStringValue(section.c_str(), "Type", GetDefaultPadType(i));
si.SetFloatValue(section.c_str(), "Deadzone", DEFAULT_STICK_DEADZONE);
si.SetFloatValue(section.c_str(), "AxisScale", DEFAULT_STICK_SCALE);
si.SetFloatValue(section.c_str(), "LargeMotorScale", DEFAULT_MOTOR_SCALE);
si.SetFloatValue(section.c_str(), "SmallMotorScale", DEFAULT_MOTOR_SCALE);
si.SetFloatValue(section.c_str(), "PressureModifier", DEFAULT_PRESSURE_MODIFIER);
si.SetStringValue(section.c_str(), "Type", type);
const ControllerInfo* ci = GetControllerInfo(type);
if (ci)
{
for (u32 i = 0; i < ci->num_settings; i++)
{
const ControllerSettingInfo& csi = ci->settings[i];
switch (csi.type)
{
case ControllerSettingInfo::Type::Boolean:
si.SetBoolValue(section.c_str(), csi.name, csi.BooleanDefaultValue());
break;
case ControllerSettingInfo::Type::Integer:
case ControllerSettingInfo::Type::IntegerList:
si.SetIntValue(section.c_str(), csi.name, csi.IntegerDefaultValue());
break;
case ControllerSettingInfo::Type::Float:
si.SetFloatValue(section.c_str(), csi.name, csi.FloatDefaultValue());
break;
case ControllerSettingInfo::Type::String:
case ControllerSettingInfo::Type::Path:
si.SetStringValue(section.c_str(), csi.name, csi.StringDefaultValue());
break;
default:
break;
}
}
}
}
// PCSX2 Controller Settings - Controller 1 / Controller 2 / ...
@ -415,26 +444,42 @@ static const PAD::ControllerBindingInfo s_dualshock2_binds[] = {
{"SmallMotor", "Small (High Frequency) Motor", PAD::ControllerBindingType::Motor, GenericInputBinding::SmallMotor},
};
static const char* s_dualshock2_invert_entries[] = {
"Not Inverted",
"Invert Left/Right",
"Invert Up/Down",
"Invert Left/Right + Up/Down",
nullptr};
static const PAD::ControllerSettingInfo s_dualshock2_settings[] = {
{PAD::ControllerSettingInfo::Type::IntegerList, "InvertL", "Invert Left Stick",
"Inverts the direction of the left analog stick.",
"0", "0", "3", nullptr, nullptr, s_dualshock2_invert_entries, 0.0f},
{PAD::ControllerSettingInfo::Type::IntegerList, "InvertR", "Invert Right Stick",
"Inverts the direction of the right analog stick.",
"0", "0", "3", nullptr, nullptr, s_dualshock2_invert_entries, 0.0f},
{PAD::ControllerSettingInfo::Type::Float, "Deadzone", "Analog Deadzone",
"Sets the analog stick deadzone, i.e. the fraction of the stick movement which will be ignored.",
"0.00", "0.00", "1.00", "0.01", "%.0f%%", 100.0f},
"0.00", "0.00", "1.00", "0.01", "%.0f%%", nullptr, 100.0f},
{PAD::ControllerSettingInfo::Type::Float, "AxisScale", "Analog Sensitivity",
"Sets the analog stick axis scaling factor. A value between 1.30 and 1.40 is recommended when using recent "
"controllers, e.g. DualShock 4, Xbox One Controller.",
"1.33", "0.01", "2.00", "0.01", "%.0f%%", 100.0f},
"1.33", "0.01", "2.00", "0.01", "%.0f%%", nullptr, 100.0f},
{PAD::ControllerSettingInfo::Type::Float, "LargeMotorScale", "Large Motor Vibration Scale",
"Increases or decreases the intensity of low frequency vibration sent by the game.",
"1.00", "0.00", "2.00", "0.01", "%.0f%%", 100.0f},
"1.00", "0.00", "2.00", "0.01", "%.0f%%", nullptr, 100.0f},
{PAD::ControllerSettingInfo::Type::Float, "SmallMotorScale", "Small Motor Vibration Scale",
"Increases or decreases the intensity of high frequency vibration sent by the game.",
"1.00", "0.00", "2.00", "0.01", "%.0f%%", 100.0f},
"1.00", "0.00", "2.00", "0.01", "%.0f%%", nullptr, 100.0f},
{PAD::ControllerSettingInfo::Type::Float, "ButtonDeadzone", "Button/Trigger Deadzone",
"Sets the deadzone for activating buttons/triggers, i.e. the fraction of the trigger which will be ignored.",
"0.00", "0.00", "1.00", "0.01", "%.0f%%", nullptr, 100.0f},
/*{PAD::ControllerSettingInfo::Type::Float, "InitialPressure", "Initial Pressure",
"Sets the pressure when the modifier button isn't held.",
"1.00", "0.01", "1.00", "0.01", "%.0f%%", 100.0f},*/
"1.00", "0.01", "1.00", "0.01", "%.0f%%", nullptr, 100.0f},*/
{PAD::ControllerSettingInfo::Type::Float, "PressureModifier", "Modifier Pressure",
"Sets the pressure when the modifier button is held.",
"0.50", "0.01", "1.00", "0.01", "%.0f%%", 100.0f},
"0.50", "0.01", "1.00", "0.01", "%.0f%%", nullptr, 100.0f},
};
static const PAD::ControllerInfo s_controller_info[] = {
@ -523,8 +568,6 @@ void PAD::CopyConfiguration(SettingsInterface* dest_si, const SettingsInterface&
dest_si->CopyBoolValue(src_si, "Pad", "MultitapPort2");
dest_si->CopyFloatValue(src_si, "Pad", "PointerXScale");
dest_si->CopyFloatValue(src_si, "Pad", "PointerYScale");
dest_si->CopyBoolValue(src_si, "Pad", "PointerXInvert");
dest_si->CopyBoolValue(src_si, "Pad", "PointerYInvert");
for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++)
{
dest_si->CopyBoolValue(src_si, "InputSources",
@ -571,6 +614,30 @@ void PAD::CopyConfiguration(SettingsInterface* dest_si, const SettingsInterface&
dest_si->CopyFloatValue(src_si, section.c_str(), "LargeMotorScale");
dest_si->CopyFloatValue(src_si, section.c_str(), "SmallMotorScale");
}
for (u32 i = 0; i < info->num_settings; i++)
{
const ControllerSettingInfo& csi = info->settings[i];
switch (csi.type)
{
case ControllerSettingInfo::Type::Boolean:
dest_si->CopyBoolValue(src_si, section.c_str(), csi.name);
break;
case ControllerSettingInfo::Type::Integer:
case ControllerSettingInfo::Type::IntegerList:
dest_si->CopyIntValue(src_si, section.c_str(), csi.name);
break;
case ControllerSettingInfo::Type::Float:
dest_si->CopyFloatValue(src_si, section.c_str(), csi.name);
break;
case ControllerSettingInfo::Type::String:
case ControllerSettingInfo::Type::Path:
dest_si->CopyStringValue(src_si, section.c_str(), csi.name);
break;
default:
break;
}
}
}
}

View File

@ -77,6 +77,7 @@ namespace PAD
{
Boolean,
Integer,
IntegerList,
Float,
String,
Path,
@ -91,6 +92,7 @@ namespace PAD
const char* max_value;
const char* step_value;
const char* format;
const char** options;
float multiplier;
const char* StringDefaultValue() const;
@ -129,6 +131,7 @@ namespace PAD
static constexpr float DEFAULT_STICK_SCALE = 1.33f;
static constexpr float DEFAULT_MOTOR_SCALE = 1.0f;
static constexpr float DEFAULT_PRESSURE_MODIFIER = 0.5f;
static constexpr float DEFAULT_BUTTON_DEADZONE = 0.0f;
/// Returns the default type for the specified port.
const char* GetDefaultPadType(u32 pad);

View File

@ -1075,8 +1075,6 @@ void Pcsx2Config::LoadSave(SettingsWrapper& wrap)
#endif
SettingsWrapBitBool(ConsoleToStdio);
SettingsWrapBitBool(HostFs);
SettingsWrapBitBool(PatchBios);
SettingsWrapEntry(PatchRegion);
SettingsWrapBitBool(BackupSavestate);
SettingsWrapBitBool(SavestateZstdCompression);
@ -1227,8 +1225,6 @@ void Pcsx2Config::CopyConfig(const Pcsx2Config& cfg)
EnableNoInterlacingPatches = cfg.EnableNoInterlacingPatches;
EnableRecordingTools = cfg.EnableRecordingTools;
UseBOOT2Injection = cfg.UseBOOT2Injection;
PatchBios = cfg.PatchBios;
PatchRegion = cfg.PatchRegion;
BackupSavestate = cfg.BackupSavestate;
SavestateZstdCompression = cfg.SavestateZstdCompression;
McdEnableEjection = cfg.McdEnableEjection;
@ -1244,6 +1240,17 @@ void Pcsx2Config::CopyConfig(const Pcsx2Config& cfg)
LimiterMode = cfg.LimiterMode;
}
void Pcsx2Config::CopyRuntimeConfig(Pcsx2Config& cfg)
{
GS.LimitScalar = cfg.GS.LimitScalar;
UseBOOT2Injection = cfg.UseBOOT2Injection;
CurrentBlockdump = std::move(cfg.CurrentBlockdump);
CurrentIRX = std::move(cfg.CurrentIRX);
CurrentGameArgs = std::move(cfg.CurrentGameArgs);
CurrentAspectRatio = cfg.CurrentAspectRatio;
LimiterMode = cfg.LimiterMode;
}
void EmuFolders::SetDefaults(SettingsInterface& si)
{
si.SetStringValue("Folders", "Bios", "bios");

View File

@ -369,8 +369,7 @@ void VMManager::ApplyGameFixes()
std::string VMManager::GetGameSettingsPath(const std::string_view& game_serial, u32 game_crc)
{
std::string sanitized_serial(game_serial);
Path::SanitizeFileName(sanitized_serial);
std::string sanitized_serial(Path::SanitizeFileName(game_serial));
return game_serial.empty() ?
Path::Combine(EmuFolders::GameSettings, fmt::format("{:08X}.ini", game_crc)) :
@ -495,7 +494,7 @@ bool VMManager::UpdateGameSettingsLayer()
}
}
Host::Internal::SetInputSettingsLayer(input_interface.get());
Host::Internal::SetInputSettingsLayer(input_interface ? input_interface.get() : Host::Internal::GetBaseSettingsLayer());
}
else
{
@ -1102,9 +1101,14 @@ void VMManager::Shutdown(bool save_resume_state)
// If the fullscreen UI is running, do a hardware reset on the GS
// so that the texture cache and targets are all cleared.
if (s_gs_open_on_initialize)
{
GetMTGS().WaitGS(false, false, false);
GetMTGS().ResetGS(true);
}
else
{
GetMTGS().WaitForClose();
}
USBshutdown();
SPU2shutdown();
@ -1718,7 +1722,11 @@ void VMManager::ApplySettings()
GetMTGS().WaitGS(false);
}
const Pcsx2Config old_config(EmuConfig);
// Reset to a clean Pcsx2Config. Otherwise things which are optional (e.g. gamefixes)
// do not use the correct default values when loading.
Pcsx2Config old_config(std::move(EmuConfig));
EmuConfig = Pcsx2Config();
EmuConfig.CopyRuntimeConfig(old_config);
LoadSettings();
CheckForConfigChanges(old_config);
}
@ -1751,37 +1759,37 @@ void VMManager::WarnAboutUnsafeSettings()
std::string messages;
if (EmuConfig.Speedhacks.fastCDVD)
messages += ICON_FA_COMPACT_DISC " Fast CDVD is enabled, this may break games.\n";
messages += ICON_FA_COMPACT_DISC " Fast CDVD is enabled, this may break games.\n";
if (EmuConfig.Speedhacks.EECycleRate != 0 || EmuConfig.Speedhacks.EECycleSkip != 0)
messages += ICON_FA_TACHOMETER_ALT " Cycle rate/skip is not at default, this may crash or make games run too slow.\n";
messages += ICON_FA_TACHOMETER_ALT " Cycle rate/skip is not at default, this may crash or make games run too slow.\n";
if (EmuConfig.SPU2.SynchMode != Pcsx2Config::SPU2Options::SynchronizationMode::TimeStretch)
messages += ICON_FA_VOLUME_MUTE " Audio is not using time stretch synchronization, this may break FMVs.\n";
messages += ICON_FA_VOLUME_MUTE " Audio is not using time stretch synchronization, this may break FMVs.\n";
if (EmuConfig.GS.HWMipmap != HWMipmapLevel::Automatic)
messages += ICON_FA_IMAGES " Mipmapping is not set to automatic. This may break rendering in some games.\n";
messages += ICON_FA_IMAGES " Mipmapping is not set to automatic. This may break rendering in some games.\n";
if (EmuConfig.GS.TextureFiltering != BiFiltering::PS2)
messages += ICON_FA_FILTER " Texture filtering is not set to Bilinear (PS2). This will break rendering in some games.\n";
messages += ICON_FA_FILTER " Texture filtering is not set to Bilinear (PS2). This will break rendering in some games.\n";
if (EmuConfig.GS.UserHacks_TriFilter != TriFiltering::Automatic)
messages += ICON_FA_PAGER " Trilinear filtering is not set to automatic. This may break rendering in some games.\n";
messages += ICON_FA_PAGER " Trilinear filtering is not set to automatic. This may break rendering in some games.\n";
if (EmuConfig.GS.AccurateBlendingUnit <= AccBlendLevel::Minimum)
messages += ICON_FA_BLENDER " Blending is below basic, this may break effects in some games.\n";
messages += ICON_FA_BLENDER " Blending is below basic, this may break effects in some games.\n";
if (EmuConfig.GS.CRCHack != CRCHackLevel::Automatic)
messages += ICON_FA_FIRST_AID " CRC Fix Level is not set to default, this may break effects in some games.\n";
messages += ICON_FA_FIRST_AID " CRC Fix Level is not set to default, this may break effects in some games.\n";
if (EmuConfig.Cpu.sseMXCSR.GetRoundMode() != SSEround_Chop || EmuConfig.Cpu.sseVUMXCSR.GetRoundMode() != SSEround_Chop)
messages += ICON_FA_MICROCHIP " EE FPU Round Mode is not set to default, this may break some games.\n";
messages += ICON_FA_MICROCHIP " EE FPU Round Mode is not set to default, this may break some games.\n";
if (!EmuConfig.Cpu.Recompiler.fpuOverflow || EmuConfig.Cpu.Recompiler.fpuExtraOverflow || EmuConfig.Cpu.Recompiler.fpuFullMode)
messages += ICON_FA_MICROCHIP " EE FPU Clamp Mode is not set to default, this may break some games.\n";
messages += ICON_FA_MICROCHIP " EE FPU Clamp Mode is not set to default, this may break some games.\n";
if (EmuConfig.Cpu.sseVUMXCSR.GetRoundMode() != SSEround_Chop)
messages += ICON_FA_MICROCHIP " VU Round Mode is not set to default, this may break some games.\n";
messages += ICON_FA_MICROCHIP " VU Round Mode is not set to default, this may break some games.\n";
if (!EmuConfig.Cpu.Recompiler.vuOverflow || EmuConfig.Cpu.Recompiler.vuExtraOverflow || EmuConfig.Cpu.Recompiler.vuSignOverflow)
messages += ICON_FA_MICROCHIP " VU Clamp Mode is not set to default, this may break some games.\n";
messages += ICON_FA_MICROCHIP " VU Clamp Mode is not set to default, this may break some games.\n";
if (!EmuConfig.EnableGameFixes)
messages += ICON_FA_GAMEPAD " Game Fixes are not enabled. Compatibility with some games may be affected.\n";
messages += ICON_FA_GAMEPAD " Game Fixes are not enabled. Compatibility with some games may be affected.\n";
if (!EmuConfig.EnablePatches)
messages += ICON_FA_GAMEPAD " Compatibility Patches are not enabled. Compatibility with some games may be affected.\n";
messages += ICON_FA_GAMEPAD " Compatibility Patches are not enabled. Compatibility with some games may be affected.\n";
if (EmuConfig.GS.FramerateNTSC != Pcsx2Config::GSOptions::DEFAULT_FRAME_RATE_NTSC)
messages += ICON_FA_TV " Frame rate for NTSC is not default. This may break some games.\n";
messages += ICON_FA_TV " Frame rate for NTSC is not default. This may break some games.\n";
if (EmuConfig.GS.FrameratePAL != Pcsx2Config::GSOptions::DEFAULT_FRAME_RATE_PAL)
messages += ICON_FA_TV " Frame rate for PAL is not default. This may break some games.\n";
messages += ICON_FA_TV " Frame rate for PAL is not default. This may break some games.\n";
if (!messages.empty())
{
@ -1796,27 +1804,27 @@ void VMManager::WarnAboutUnsafeSettings()
messages.clear();
if (!EmuConfig.Cpu.Recompiler.EnableEE)
messages += ICON_FA_EXCLAMATION_CIRCLE " EE Recompiler is not enabled, this will significantly reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " EE Recompiler is not enabled, this will significantly reduce performance.\n";
if (!EmuConfig.Cpu.Recompiler.EnableVU0)
messages += ICON_FA_EXCLAMATION_CIRCLE " VU0 Recompiler is not enabled, this will significantly reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " VU0 Recompiler is not enabled, this will significantly reduce performance.\n";
if (!EmuConfig.Cpu.Recompiler.EnableVU1)
messages += ICON_FA_EXCLAMATION_CIRCLE " VU1 Recompiler is not enabled, this will significantly reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " VU1 Recompiler is not enabled, this will significantly reduce performance.\n";
if (!EmuConfig.Cpu.Recompiler.EnableIOP)
messages += ICON_FA_EXCLAMATION_CIRCLE " IOP Recompiler is not enabled, this will significantly reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " IOP Recompiler is not enabled, this will significantly reduce performance.\n";
if (EmuConfig.Cpu.Recompiler.EnableEECache)
messages += ICON_FA_EXCLAMATION_CIRCLE " EE Cache is enabled, this will significantly reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " EE Cache is enabled, this will significantly reduce performance.\n";
if (!EmuConfig.Speedhacks.WaitLoop)
messages += ICON_FA_EXCLAMATION_CIRCLE " EE Wait Loop Detection is not enabled, this may reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " EE Wait Loop Detection is not enabled, this may reduce performance.\n";
if (!EmuConfig.Speedhacks.IntcStat)
messages += ICON_FA_EXCLAMATION_CIRCLE " INTC Spin Detection is not enabled, this may reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " INTC Spin Detection is not enabled, this may reduce performance.\n";
if (!EmuConfig.Speedhacks.vu1Instant)
messages += ICON_FA_EXCLAMATION_CIRCLE " Instant VU1 is disabled, this may reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " Instant VU1 is disabled, this may reduce performance.\n";
if (!EmuConfig.Speedhacks.vuFlagHack)
messages += ICON_FA_EXCLAMATION_CIRCLE " mVU Flag Hack is not enabled, this may reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " mVU Flag Hack is not enabled, this may reduce performance.\n";
if (EmuConfig.GS.GPUPaletteConversion)
messages += ICON_FA_EXCLAMATION_CIRCLE " GPU Palette Conversion is enabled, this may reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " GPU Palette Conversion is enabled, this may reduce performance.\n";
if (EmuConfig.GS.TexturePreloading != TexturePreloadingLevel::Full)
messages += ICON_FA_EXCLAMATION_CIRCLE " Texture Preloading is not Full, this may reduce performance.\n";
messages += ICON_FA_EXCLAMATION_CIRCLE " Texture Preloading is not Full, this may reduce performance.\n";
if (!messages.empty())
{

View File

@ -57,7 +57,6 @@ std::string BiosDescription;
std::string BiosZone;
std::string BiosPath;
BiosDebugInformation CurrentBiosInformation;
s64 BiosRegionOffset = 0;
static bool LoadBiosVersion(const char* filename, std::FILE* fp, u32& version, std::string& description, u32& region, std::string& zone)
{
@ -92,25 +91,19 @@ static bool LoadBiosVersion(const char* filename, std::FILE* fp, u32& version, s
switch (romver[4])
{
// clang-format off
case 'T': region = 0; break;
case 'X': region = 1; break;
case 'J': region = 2; break;
case 'A': region = 3; break;
case 'E': region = 4; break;
case 'H': region = 5; break;
case 'P': region = 6; break;
case 'C': region = 7; break;
case 'T': zone = "T10K"; region = 0; break;
case 'X': zone = "Test"; region = 1; break;
case 'J': zone = "Japan"; region = 2; break;
case 'A': zone = "USA"; region = 3; break;
case 'E': zone = "Europe"; region = 4; break;
case 'H': zone = "HK"; region = 5; break;
case 'P': zone = "Free"; region = 6; break;
case 'C': zone = "China"; region = 7; break;
// clang-format on
}
if (region <= 7)
{
zone = BiosZoneStrings[region];
}
else
{
zone.clear();
zone += romver[4];
default:
zone.clear();
zone += romver[4];
break;
}
char vermaj[3] = {romver[0], romver[1], 0};
@ -128,7 +121,6 @@ static bool LoadBiosVersion(const char* filename, std::FILE* fp, u32& version, s
version = strtol(vermaj, (char**)NULL, 0) << 8;
version |= strtol(vermin, (char**)NULL, 0);
foundRomVer = true;
BiosRegionOffset = fileOffset;
Console.WriteLn("Bios Found: %s", description.c_str());
}
@ -301,13 +293,6 @@ bool LoadBIOS()
ChecksumIt(BiosChecksum, eeMem->ROM);
BiosPath = std::move(path);
// Patch the region
if (EmuConfig.PatchBios)
{
eeMem->ROM[BiosRegionOffset + 4] = EmuConfig.PatchRegion[0];
Console.WriteLn("Patching ROM with region code %c", EmuConfig.PatchRegion[0]);
}
#ifndef PCSX2_CORE
Console.SetTitle(StringUtil::StdStringFromFormat("Running BIOS (%s v%u.%u)",
BiosZone.c_str(), BiosVersion >> 8, BiosVersion & 0xff).c_str());

View File

@ -28,24 +28,6 @@ struct BiosDebugInformation
u32 threadListAddr;
};
// The following two arrays are used for Qt
[[maybe_unused]] static const char* BiosZoneStrings[] {
"T10K",
"Test",
"Japan",
"USA",
"Europe",
"HK",
"Free",
"China",
nullptr
};
[[maybe_unused]] static const char* BiosZoneBytes[]
{
"T", "X", "J", "A", "E", "H", "P", "C", nullptr
};
extern BiosDebugInformation CurrentBiosInformation;
extern u32 BiosVersion; // Used by CDVD
extern u32 BiosRegion; // Used by CDVD
@ -59,3 +41,4 @@ extern std::string BiosPath;
extern bool LoadBIOS();
extern bool IsBIOS(const char* filename, u32& version, std::string& description, u32& region, std::string& zone);
extern bool IsBIOSAvailable(const std::string& full_path);