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