diff --git a/OptiScaler/OptiScaler.vcxproj b/OptiScaler/OptiScaler.vcxproj index 9539e9db..bcd71d8c 100644 --- a/OptiScaler/OptiScaler.vcxproj +++ b/OptiScaler/OptiScaler.vcxproj @@ -199,7 +199,7 @@ Windows true false - WindowsApp.lib;Dbghelp.lib;dxgi.lib;d3d11.lib;d3d12.lib;vulkan-1.lib;dxguid.lib;freetype.lib;ffx_fsr2_api_x64d.lib;ffx_fsr2_api_dx11_x64d.lib;ffx_fsr2_api_dx12_x64d.lib;ffx_fsr2_api_vk_x64d.lib;ffx_fsr2_212_api_dx12_x64d.lib;ffx_fsr2_212_api_vk_x64d.lib;ffx_fsr2_212_api_x64d.lib;d3dcompiler.lib;d3dx11.lib;ffx_backend_dx11_x64d.lib;ffx_fsr3_x64d.lib;ffx_fsr3upscaler_x64d.lib;ffx_opticalflow_x64d.lib;ffx_frameinterpolation_x64d.lib;%(AdditionalDependencies) + winhttp.lib;WindowsApp.lib;Dbghelp.lib;dxgi.lib;d3d11.lib;d3d12.lib;vulkan-1.lib;dxguid.lib;freetype.lib;ffx_fsr2_api_x64d.lib;ffx_fsr2_api_dx11_x64d.lib;ffx_fsr2_api_dx12_x64d.lib;ffx_fsr2_api_vk_x64d.lib;ffx_fsr2_212_api_dx12_x64d.lib;ffx_fsr2_212_api_vk_x64d.lib;ffx_fsr2_212_api_x64d.lib;d3dcompiler.lib;d3dx11.lib;ffx_backend_dx11_x64d.lib;ffx_fsr3_x64d.lib;ffx_fsr3upscaler_x64d.lib;ffx_opticalflow_x64d.lib;ffx_frameinterpolation_x64d.lib;%(AdditionalDependencies) Source.def vulkan-1.dll;dxgi.dll;d3d12.dll;D3DCOMPILER_47.dll;Dbghelp.dll;d3d11.dll @@ -234,7 +234,7 @@ true false false - WindowsApp.lib;Dbghelp.lib;dxgi.lib;d3d11.lib;d3d12.lib;vulkan-1.lib;dxguid.lib;freetype.lib;ffx_fsr2_api_x64.lib;ffx_fsr2_api_dx11_x64.lib;ffx_fsr2_api_dx12_x64.lib;ffx_fsr2_api_vk_x64.lib;ffx_fsr2_212_api_dx12_x64.lib;ffx_fsr2_212_api_vk_x64.lib;ffx_fsr2_212_api_x64.lib;d3dcompiler.lib;ffx_backend_dx11_x64.lib;ffx_fsr3_x64.lib;ffx_frameinterpolation_x64.lib;ffx_fsr3upscaler_x64.lib;ffx_opticalflow_x64.lib;%(AdditionalDependencies) + winhttp.lib;WindowsApp.lib;Dbghelp.lib;dxgi.lib;d3d11.lib;d3d12.lib;vulkan-1.lib;dxguid.lib;freetype.lib;ffx_fsr2_api_x64.lib;ffx_fsr2_api_dx11_x64.lib;ffx_fsr2_api_dx12_x64.lib;ffx_fsr2_api_vk_x64.lib;ffx_fsr2_212_api_dx12_x64.lib;ffx_fsr2_212_api_vk_x64.lib;ffx_fsr2_212_api_x64.lib;d3dcompiler.lib;ffx_backend_dx11_x64.lib;ffx_fsr3_x64.lib;ffx_frameinterpolation_x64.lib;ffx_fsr3upscaler_x64.lib;ffx_opticalflow_x64.lib;%(AdditionalDependencies) Source.def vulkan-1.dll;dxgi.dll;d3d12.dll;D3DCOMPILER_47.dll;Dbghelp.dll;d3d11.dll @@ -282,7 +282,7 @@ copy NUL "$(SolutionDir)x64\Release\a\!! EXTRACT ALL FILES TO GAME FOLDER !!" /Y true true false - WindowsApp.lib;Dbghelp.lib;dxgi.lib;d3d11.lib;d3d12.lib;vulkan-1.lib;dxguid.lib;freetype.lib;ffx_fsr2_api_x64.lib;ffx_fsr2_api_dx11_x64.lib;ffx_fsr2_api_dx12_x64.lib;ffx_fsr2_api_vk_x64.lib;ffx_fsr2_212_api_dx12_x64.lib;ffx_fsr2_212_api_vk_x64.lib;ffx_fsr2_212_api_x64.lib;d3dcompiler.lib;ffx_backend_dx11_x64.lib;ffx_fsr3_x64.lib;ffx_frameinterpolation_x64.lib;ffx_fsr3upscaler_x64.lib;ffx_opticalflow_x64.lib;%(AdditionalDependencies) + winhttp.lib;WindowsApp.lib;Dbghelp.lib;dxgi.lib;d3d11.lib;d3d12.lib;vulkan-1.lib;dxguid.lib;freetype.lib;ffx_fsr2_api_x64.lib;ffx_fsr2_api_dx11_x64.lib;ffx_fsr2_api_dx12_x64.lib;ffx_fsr2_api_vk_x64.lib;ffx_fsr2_212_api_dx12_x64.lib;ffx_fsr2_212_api_vk_x64.lib;ffx_fsr2_212_api_x64.lib;d3dcompiler.lib;ffx_backend_dx11_x64.lib;ffx_fsr3_x64.lib;ffx_frameinterpolation_x64.lib;ffx_fsr3upscaler_x64.lib;ffx_opticalflow_x64.lib;%(AdditionalDependencies) Source.def vulkan-1.dll;dxgi.dll;d3d12.dll;D3DCOMPILER_47.dll;Dbghelp.dll;d3d11.dll @@ -458,6 +458,7 @@ copy NUL "$(SolutionDir)x64\Release\a\!! EXTRACT ALL FILES TO GAME FOLDER !!" /Y + @@ -558,6 +559,7 @@ copy NUL "$(SolutionDir)x64\Release\a\!! EXTRACT ALL FILES TO GAME FOLDER !!" /Y + diff --git a/OptiScaler/OptiScaler.vcxproj.filters b/OptiScaler/OptiScaler.vcxproj.filters index fc94e237..f8f585c9 100644 --- a/OptiScaler/OptiScaler.vcxproj.filters +++ b/OptiScaler/OptiScaler.vcxproj.filters @@ -383,6 +383,9 @@ Header Files + + Header Files + Header Files @@ -512,6 +515,21 @@ Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + @@ -521,6 +539,9 @@ Util + + Source Files + Source Files @@ -803,6 +824,9 @@ Source Files + + Source Files + diff --git a/OptiScaler/State.h b/OptiScaler/State.h index f849473b..9e52fb41 100644 --- a/OptiScaler/State.h +++ b/OptiScaler/State.h @@ -8,6 +8,7 @@ #include #include #include +#include typedef enum API { @@ -154,6 +155,15 @@ class State double lastFrameTime = 0.0; std::mutex frameTimeMutex; + // Version check + std::mutex versionCheckMutex; + bool versionCheckInProgress = false; + bool versionCheckCompleted = false; + bool updateAvailable = false; + std::string latestVersionTag; + std::string latestVersionUrl; + std::string versionCheckError; + // Swapchain info float screenWidth = 800.0; float screenHeight = 450.0; diff --git a/OptiScaler/dllmain.cpp b/OptiScaler/dllmain.cpp index 56f38f54..58b8fc4a 100644 --- a/OptiScaler/dllmain.cpp +++ b/OptiScaler/dllmain.cpp @@ -35,6 +35,7 @@ #include #include +#include static std::vector _asiHandles; @@ -933,6 +934,8 @@ static void CheckWorkingMode() else LOG_ERROR("Failed to load igdext64.dll"); } + + VersionCheck::Start(); } return; diff --git a/OptiScaler/menu/menu_common.cpp b/OptiScaler/menu/menu_common.cpp index 22caf13f..f1b16c48 100644 --- a/OptiScaler/menu/menu_common.cpp +++ b/OptiScaler/menu/menu_common.cpp @@ -16,6 +16,10 @@ #include +#include + +#include + #define MARK_ALL_BACKENDS_CHANGED() \ for (auto& singleChangeBackend : State::Instance().changeBackend) \ singleChangeBackend.second = true; @@ -78,6 +82,14 @@ static std::vector splashText = { "May the coping commence...", "FSR4 DP4a wenETA, AMD plz", "OptiCopers, assemble!" }; +static ImVec2 updateNoticePosition(-1000.0f, -1000.0f); +static ImVec2 updateNoticeSize(0.0f, 0.0f); +static double updateNoticeStart = 0.0; +static double updateNoticeLimit = 0.0; +static bool updateNoticeVisible = false; +static std::string updateNoticeTag; +static std::string updateNoticeUrl; + void MenuCommon::ShowTooltip(const char* tip) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) @@ -515,7 +527,10 @@ class Keybind int id; bool waitingForKey = false; - std::string KeyNameFromVirtualKeyCode(USHORT virtualKey) + public: + Keybind(std::string name, int id) : name(name), id(id) {} + + static std::string KeyNameFromVirtualKeyCode(USHORT virtualKey) { if (virtualKey == (USHORT) UnboundKey) return "Unbound"; @@ -554,9 +569,6 @@ class Keybind return "Unknown"; } - public: - Keybind(std::string name, int id) : name(name), id(id) {} - void Render(CustomOptional& configKey) { ImGui::PushID(id); @@ -1382,12 +1394,60 @@ bool MenuCommon::RenderMenu() inputFpsCycle = false; } - bool frameStarted = false; bool frameTimesCalculated = false; const double splashTime = 7000.0; const double fadeTime = 1000.0; + const double updateNoticeTime = 60000.0; + const double updateNoticeFade = 1000.0; static std::string splashMessage; + struct VersionCheckStatus + { + bool completed = false; + bool updateAvailable = false; + std::string latestTag; + std::string latestUrl; + std::string error; + } versionStatus; + + { + auto& state = State::Instance(); + std::scoped_lock lock(state.versionCheckMutex); + versionStatus.completed = state.versionCheckCompleted; + versionStatus.updateAvailable = state.updateAvailable; + versionStatus.latestTag = state.latestVersionTag; + versionStatus.latestUrl = state.latestVersionUrl; + versionStatus.error = state.versionCheckError; + } + + const auto& currentVersionText = VersionCheck::CurrentVersionString(); + + if (versionStatus.completed && versionStatus.updateAvailable && !versionStatus.latestTag.empty()) + { + if (updateNoticeTag != versionStatus.latestTag) + { + updateNoticeTag = versionStatus.latestTag; + updateNoticeUrl = versionStatus.latestUrl; + updateNoticeStart = now; + updateNoticeLimit = updateNoticeStart + updateNoticeTime; + updateNoticeVisible = true; + } + } + + if (!_isUWP) + { + ImGui_ImplWin32_NewFrame(); + } + else if (!newFrame) + { + ImVec2 displaySize { State::Instance().screenWidth, State::Instance().screenHeight }; + ImGui_ImplUwp_NewFrame(displaySize); + } + + MenuHdrCheck(io); + MenuSizeCheck(io); + ImGui::NewFrame(); + // Splash screen if (!Config::Instance()->DisableSplash.value_or_default()) { @@ -1402,19 +1462,6 @@ bool MenuCommon::RenderMenu() if (now > splashStart && now < splashLimit) { - if (!_isUWP) - { - ImGui_ImplWin32_NewFrame(); - } - else if (!newFrame) - { - ImVec2 displaySize { State::Instance().screenWidth, State::Instance().screenHeight }; - ImGui_ImplUwp_NewFrame(displaySize); - } - - MenuHdrCheck(io); - MenuSizeCheck(io); - ImGui::NewFrame(); ImGui::SetNextWindowSize({ 0.0f, 0.0f }); ImGui::SetNextWindowBgAlpha(Config::Instance()->FpsOverlayAlpha.value_or_default()); @@ -1463,14 +1510,80 @@ bool MenuCommon::RenderMenu() ImGui::PopStyleColor(2); ImGui::PopStyleVar(2); + } + } - frameStarted = true; + if (updateNoticeVisible) + { + if (now >= updateNoticeLimit) + { + updateNoticeVisible = false; + } + else + { + ImGui::SetNextWindowSize({ 0.0f, 0.0f }); + ImGui::SetNextWindowBgAlpha(Config::Instance()->FpsOverlayAlpha.value_or_default()); + ImGui::SetNextWindowPos(updateNoticePosition, ImGuiCond_Always); + + float windowAlpha = 1.0f; + if (auto diff = now - updateNoticeStart; diff < updateNoticeFade) + windowAlpha = static_cast(diff / updateNoticeFade); + else if (auto diff = updateNoticeLimit - now; diff < updateNoticeFade) + windowAlpha = static_cast(diff / updateNoticeFade); + + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, windowAlpha); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12, 8)); + ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(0, 0, 0, 0)); - if (!_isVisible && !Config::Instance()->ShowFps.value_or_default()) + bool pushedFont = false; + if (ImGui::Begin("Update Available", nullptr, + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav)) { - ImGui::EndFrame(); - return true; + float splashScale = 1.0f; + float baseScaleHeight = 720.0f; + + if (io.DisplaySize.y > baseScaleHeight) + splashScale = io.DisplaySize.y / baseScaleHeight; + + if (Config::Instance()->UseHQFont.value_or_default()) + { + ImGui::PushFontSize(std::round(splashScale * fontSize)); + pushedFont = true; + } + else + { + ImGui::SetWindowFontScale(splashScale); + } + + ImGui::TextColored(toneMapColor(ImVec4(1.0f, 0.0f, 0.0f, 1.0f)), "OptiScaler Update available"); + ImGui::Spacing(); + ImGui::Text( + "Press %s for more info", + Keybind::KeyNameFromVirtualKeyCode(Config::Instance()->ShortcutKey.value_or_default()).c_str()); + + if (pushedFont) + ImGui::PopFontSize(); } + + updateNoticeSize = ImGui::GetWindowSize(); + ImGui::End(); + + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(2); + + updateNoticePosition.x = 0.0f; + float baseY = io.DisplaySize.y - updateNoticeSize.y; + + if (!Config::Instance()->DisableSplash.value_or_default() && now > splashStart && now < splashLimit) + baseY = splashPosition.y - updateNoticeSize.y - 10.0f; + + if (baseY < 0.0f) + baseY = 0.0f; + + updateNoticePosition.y = baseY; } } @@ -1495,25 +1608,6 @@ bool MenuCommon::RenderMenu() frameRate = 1000.0 / frameTime; frameTimesCalculated = true; - if (!frameStarted) - { - if (!_isUWP) - { - ImGui_ImplWin32_NewFrame(); - } - else if (!newFrame) - { - ImVec2 displaySize { State::Instance().screenWidth, State::Instance().screenHeight }; - ImGui_ImplUwp_NewFrame(displaySize); - } - - MenuHdrCheck(io); - MenuSizeCheck(io); - ImGui::NewFrame(); - - frameStarted = true; - } - State::Instance().frameTimeMutex.lock(); std::vector frameTimeArray(State::Instance().frameTimes.begin(), State::Instance().frameTimes.end()); std::vector upscalerFrameTimeArray(State::Instance().upscaleTimes.begin(), @@ -1772,17 +1866,9 @@ bool MenuCommon::RenderMenu() else overlayPosition.y = io.DisplaySize.y - overlaySize.y; } - - if (!_isVisible) - { - ImGui::EndFrame(); - return true; - } } - if (!_isVisible) - return false; - + if (_isVisible) { // Overlay font if (Config::Instance()->UseHQFont.value_or_default()) @@ -1806,23 +1892,6 @@ bool MenuCommon::RenderMenu() frameRate = 1000.0 / frameTime; } - if (!frameStarted) - { - if (!_isUWP) - { - ImGui_ImplWin32_NewFrame(); - } - else if (!newFrame) - { - ImVec2 displaySize { State::Instance().screenWidth, State::Instance().screenHeight }; - ImGui_ImplUwp_NewFrame(displaySize); - } - - MenuHdrCheck(io); - MenuSizeCheck(io); - ImGui::NewFrame(); - } - ImGuiWindowFlags flags = 0; flags |= ImGuiWindowFlags_NoSavedSettings; flags |= ImGuiWindowFlags_NoCollapse; @@ -1877,6 +1946,30 @@ bool MenuCommon::RenderMenu() _selectedScale = ((int) (Config::Instance()->MenuScale.value() * 10.0f)) - 5; + if (versionStatus.completed) + { + if (versionStatus.updateAvailable && !versionStatus.latestTag.empty()) + { + ImGui::Spacing(); + ImGui::TextColored(ImVec4(1.f, 0.8f, 0.f, 1.f), "Update available: %s (current %s)", + versionStatus.latestTag.c_str(), currentVersionText.c_str()); + + if (!versionStatus.latestUrl.empty()) + { + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Open release page", versionStatus.latestUrl.c_str()); + } + + ImGui::Spacing(); + } + else if (!versionStatus.error.empty()) + { + ImGui::Spacing(); + ImGui::TextColored(ImVec4(1.f, 0.4f, 0.f, 1.f), "%s", versionStatus.error.c_str()); + ImGui::Spacing(); + } + } + // No active upscaler message if (currentFeature == nullptr || !currentFeature->IsInited()) { @@ -4578,6 +4671,8 @@ bool MenuCommon::RenderMenu() if (Config::Instance()->UseHQFont.value_or_default()) ImGui::PopFontSize(); + ImGui::EndFrame(); + return true; } } diff --git a/OptiScaler/version_check.cpp b/OptiScaler/version_check.cpp new file mode 100644 index 00000000..6789f645 --- /dev/null +++ b/OptiScaler/version_check.cpp @@ -0,0 +1,338 @@ +#include "pch.h" + +#include "version_check.h" + +#include "State.h" +#include "resource.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +struct SemanticVersion +{ + int major = 0; + int minor = 0; + int patch = 0; +}; + +struct LatestReleaseInfo +{ + std::string tag; + std::string url; +}; + +SemanticVersion CurrentVersion() { return { VER_MAJOR_VERSION, VER_MINOR_VERSION, VER_HOTFIX_VERSION }; } + +std::optional ParseVersionString(std::string_view version) +{ + if (version.empty()) + return std::nullopt; + + if (version.front() == 'v' || version.front() == 'V') + version.remove_prefix(1); + + const auto suffixPos = version.find_first_of("-+ "); + if (suffixPos != std::string_view::npos) + version = version.substr(0, suffixPos); + + SemanticVersion parsed {}; + int components[3] = { 0, 0, 0 }; + size_t componentIndex = 0; + size_t begin = 0; + + while (begin <= version.size() && componentIndex < 3) + { + const size_t end = version.find('.', begin); + const auto part = version.substr(begin, (end == std::string_view::npos ? version.size() : end) - begin); + + if (part.empty()) + return std::nullopt; + + int value = 0; + const auto result = std::from_chars(part.data(), part.data() + part.size(), value); + if (result.ec != std::errc()) + return std::nullopt; + + components[componentIndex++] = value; + + if (end == std::string_view::npos) + break; + + begin = end + 1; + } + + parsed.major = components[0]; + parsed.minor = components[1]; + parsed.patch = components[2]; + + return parsed; +} + +int CompareVersions(const SemanticVersion& lhs, const SemanticVersion& rhs) +{ + if (lhs.major != rhs.major) + return lhs.major < rhs.major ? -1 : 1; + if (lhs.minor != rhs.minor) + return lhs.minor < rhs.minor ? -1 : 1; + if (lhs.patch != rhs.patch) + return lhs.patch < rhs.patch ? -1 : 1; + return 0; +} + +std::optional FetchLatestRelease() +{ + HINTERNET session = nullptr; + HINTERNET connection = nullptr; + HINTERNET request = nullptr; + + auto cleanup = [&]() + { + if (request != nullptr) + { + WinHttpCloseHandle(request); + request = nullptr; + } + if (connection != nullptr) + { + WinHttpCloseHandle(connection); + connection = nullptr; + } + if (session != nullptr) + { + WinHttpCloseHandle(session); + session = nullptr; + } + }; + + session = WinHttpOpen(L"OptiScaler Version Check/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, 0); + if (session == nullptr) + { + LOG_WARN("Version check failed to open WinHTTP session: {}", GetLastError()); + cleanup(); + return std::nullopt; + } + + WinHttpSetTimeouts(session, 5000, 5000, 5000, 5000); + + connection = WinHttpConnect(session, L"api.github.com", INTERNET_DEFAULT_HTTPS_PORT, 0); + if (connection == nullptr) + { + LOG_WARN("Version check failed to connect: {}", GetLastError()); + cleanup(); + return std::nullopt; + } + + request = WinHttpOpenRequest(connection, L"GET", L"/repos/optiscaler/optiscaler/releases/latest", nullptr, + WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE); + if (request == nullptr) + { + LOG_WARN("Version check failed to open request: {}", GetLastError()); + cleanup(); + return std::nullopt; + } + + LPCWSTR headers = L"User-Agent: OptiScaler\r\nAccept: application/vnd.github+json\r\nAccept-Encoding: identity\r\n"; + if (!WinHttpSendRequest(request, headers, (DWORD) -1, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) + { + LOG_WARN("Version check failed to send request: {}", GetLastError()); + cleanup(); + return std::nullopt; + } + + if (!WinHttpReceiveResponse(request, nullptr)) + { + LOG_WARN("Version check failed to receive response: {}", GetLastError()); + cleanup(); + return std::nullopt; + } + + DWORD statusCode = 0; + DWORD statusCodeSize = sizeof(statusCode); + if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX)) + { + LOG_WARN("Version check failed to read status code: {}", GetLastError()); + cleanup(); + return std::nullopt; + } + + if (statusCode != HTTP_STATUS_OK) + { + LOG_WARN("Version check returned HTTP status {}", statusCode); + cleanup(); + return std::nullopt; + } + + std::string response; + DWORD available = 0; + do + { + if (!WinHttpQueryDataAvailable(request, &available)) + { + LOG_WARN("Version check failed querying data availability: {}", GetLastError()); + cleanup(); + return std::nullopt; + } + + if (available == 0) + break; + + std::string buffer; + buffer.resize(available); + DWORD downloaded = 0; + if (!WinHttpReadData(request, buffer.data(), available, &downloaded)) + { + LOG_WARN("Version check failed reading response: {}", GetLastError()); + cleanup(); + return std::nullopt; + } + + response.append(buffer.data(), downloaded); + } while (available > 0); + + cleanup(); + + try + { + auto json = nlohmann::json::parse(response); + LatestReleaseInfo info; + info.tag = json.value("tag_name", std::string {}); + info.url = json.value("html_url", std::string {}); + + if (info.tag.empty()) + return std::nullopt; + + return info; + } + catch (const std::exception& ex) + { + LOG_WARN("Version check failed to parse response: {}", ex.what()); + return std::nullopt; + } +} + +void FinishVersionCheck() +{ + auto& state = State::Instance(); + std::scoped_lock lock(state.versionCheckMutex); + state.versionCheckInProgress = false; + state.versionCheckCompleted = true; +} + +void RunVersionCheck() +{ + struct Finalizer + { + ~Finalizer() { FinishVersionCheck(); } + } finalize; + + auto release = FetchLatestRelease(); + if (!release.has_value()) + { + auto& state = State::Instance(); + std::scoped_lock lock(state.versionCheckMutex); + state.versionCheckError = "Unable to check for updates."; + state.updateAvailable = false; + return; + } + + const auto remoteVersion = ParseVersionString(release->tag); + + auto& state = State::Instance(); + { + std::scoped_lock lock(state.versionCheckMutex); + state.latestVersionTag = release->tag; + state.latestVersionUrl = release->url; + } + + if (!remoteVersion.has_value()) + { + LOG_WARN("Version check received unrecognized tag format: {}", release->tag); + std::scoped_lock lock(state.versionCheckMutex); + state.versionCheckError = "Received an unknown version format from update server."; + state.updateAvailable = false; + return; + } + + const auto localVersion = CurrentVersion(); + const bool updateAvailable = CompareVersions(remoteVersion.value(), localVersion) > 0; + + { + std::scoped_lock lock(state.versionCheckMutex); + state.updateAvailable = updateAvailable; + state.versionCheckError.clear(); + } + + if (updateAvailable) + { + LOG_WARN("New OptiScaler release available: {} (current {}.{}.{}).", release->tag, VER_MAJOR_VERSION, + VER_MINOR_VERSION, VER_HOTFIX_VERSION); + } + else + { + LOG_INFO("OptiScaler is up to date (current {}.{}.{})", VER_MAJOR_VERSION, VER_MINOR_VERSION, + VER_HOTFIX_VERSION); + } +} +} // namespace + +const std::string& VersionCheck::CurrentVersionString() +{ + static const std::string version = + std::format("{}.{}.{}", VER_MAJOR_VERSION, VER_MINOR_VERSION, VER_HOTFIX_VERSION); + return version; +} + +void VersionCheck::Start() +{ + auto& state = State::Instance(); + { + std::scoped_lock lock(state.versionCheckMutex); + if (state.versionCheckInProgress || state.versionCheckCompleted) + return; + + state.versionCheckInProgress = true; + state.versionCheckCompleted = false; + state.updateAvailable = false; + state.versionCheckError.clear(); + state.latestVersionTag.clear(); + state.latestVersionUrl.clear(); + } + + std::thread( + []() + { + try + { + RunVersionCheck(); + } + catch (const std::exception& ex) + { + LOG_ERROR("Version check failed with exception: {}", ex.what()); + auto& state = State::Instance(); + std::scoped_lock lock(state.versionCheckMutex); + state.versionCheckError = "Update check failed."; + state.updateAvailable = false; + } + catch (...) + { + LOG_ERROR("Version check failed with unknown exception"); + auto& state = State::Instance(); + std::scoped_lock lock(state.versionCheckMutex); + state.versionCheckError = "Update check failed."; + state.updateAvailable = false; + } + }) + .detach(); +} diff --git a/OptiScaler/version_check.h b/OptiScaler/version_check.h new file mode 100644 index 00000000..750f4f38 --- /dev/null +++ b/OptiScaler/version_check.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace VersionCheck +{ +const std::string& CurrentVersionString(); +void Start(); +} // namespace VersionCheck