diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index 1c4306172..bb44f73ef 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -179,7 +179,10 @@ target_compile_definitions(duckstation-qt PRIVATE QT_NO_EXCEPTIONS)
add_core_resources(duckstation-qt)
if(WIN32)
- target_sources(duckstation-qt PRIVATE duckstation-qt.rc)
+ target_sources(duckstation-qt PRIVATE
+ duckstation-qt.rc
+ vcruntimecheck.cpp
+ )
# We want a Windows subsystem application not console.
set_target_properties(duckstation-qt PROPERTIES
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index 07e34f8ee..1696c6390 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -51,6 +51,7 @@
+
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters
index 25961d409..cfe5d458f 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj.filters
+++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters
@@ -178,6 +178,7 @@
moc
+
diff --git a/src/duckstation-qt/vcruntimecheck.cpp b/src/duckstation-qt/vcruntimecheck.cpp
new file mode 100644
index 000000000..dc892c17b
--- /dev/null
+++ b/src/duckstation-qt/vcruntimecheck.cpp
@@ -0,0 +1,108 @@
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin
+// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
+
+#include "common/windows_headers.h"
+#include
+
+#include "fmt/format.h"
+
+// Minimum version is 14.38.33135.0.
+static constexpr u32 MIN_VERSION_V0 = 14;
+static constexpr u32 MIN_VERSION_V1 = 38;
+static constexpr u32 MIN_VERSION_V2 = 33135;
+static constexpr u32 MIN_VERSION_V3 = 0;
+static constexpr const char* DOWNLOAD_URL = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
+
+struct VCRuntimeCheckObject
+{
+ VCRuntimeCheckObject()
+ {
+ const HMODULE crt_handle = GetModuleHandleW(L"msvcp140.dll");
+ if (!crt_handle)
+ return;
+
+ const HANDLE heap = GetProcessHeap();
+ DWORD filename_length = MAX_PATH;
+ LPWSTR filename = static_cast(HeapAlloc(heap, 0, filename_length));
+ if (!filename)
+ return;
+
+ for (;;)
+ {
+ DWORD len = GetModuleFileNameW(crt_handle, filename, filename_length);
+ if (len == filename_length && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
+ {
+ filename_length *= 2;
+ if (filename_length >= 4 * 1024)
+ return;
+ LPWSTR new_filename = static_cast(HeapReAlloc(heap, 0, filename, filename_length));
+ if (!new_filename)
+ {
+ HeapFree(heap, 0, filename);
+ return;
+ }
+ filename = new_filename;
+ continue;
+ }
+
+ break;
+ }
+
+ const DWORD version_size = GetFileVersionInfoSizeExW(0, filename, nullptr);
+ LPVOID version_block;
+ if (version_size == 0 || !(version_block = HeapAlloc(heap, 0, version_size)))
+ {
+ HeapFree(heap, 0, filename);
+ return;
+ }
+
+ VS_FIXEDFILEINFO* fi;
+ UINT fi_size;
+ if (!GetFileVersionInfoExW(0, filename, 0, version_size, version_block) ||
+ !VerQueryValueW(version_block, L"\\", reinterpret_cast(&fi), &fi_size))
+ {
+ HeapFree(heap, 0, version_block);
+ HeapFree(heap, 0, filename);
+ return;
+ }
+
+ const DWORD v0 = (fi->dwFileVersionMS >> 16) & 0xFFFFu;
+ const DWORD v1 = fi->dwFileVersionMS & 0xFFFFu;
+ const DWORD v2 = (fi->dwFileVersionLS >> 16) & 0xFFFFu;
+ const DWORD v3 = fi->dwFileVersionLS & 0xFFFFu;
+
+ HeapFree(heap, 0, version_block);
+ HeapFree(heap, 0, filename);
+
+ if (v0 >= MIN_VERSION_V0 && v1 >= MIN_VERSION_V1 && v2 >= MIN_VERSION_V2)
+ return;
+
+ // fmt is self-contained, hopefully it'll be okay.
+ char message[512];
+ const auto fmt_result =
+ fmt::format_to_n(message, sizeof(message),
+ "Your Microsoft Visual C++ Runtime appears to be too old for this build of DuckStation.\n\n"
+ "Your version: {}.{}.{}.{}\n"
+ "Required version: {}.{}.{}.{}\n\n"
+ "You can download the latest version from {}.\n\n"
+ "Do you want to exit and download this version now?\n"
+ "If you select No, DuckStation will likely crash.",
+ v0, v1, v2, v3, MIN_VERSION_V0, MIN_VERSION_V1, MIN_VERSION_V2, MIN_VERSION_V3, DOWNLOAD_URL);
+ message[(fmt_result.size > (sizeof(message) - 1)) ? (sizeof(message) - 1) : fmt_result.size] = 0;
+
+ if (MessageBoxA(NULL, message, "Old Visual C++ Runtime Detected", MB_ICONERROR | MB_YESNO) == IDNO)
+ return;
+
+ if (!ShellExecuteA(NULL, "open", DOWNLOAD_URL, nullptr, nullptr, SW_SHOWNORMAL))
+ MessageBoxA(NULL, "ShellExecuteA() failed, you may need to manually open the URL.", "Error", MB_OK);
+
+ TerminateProcess(GetCurrentProcess(), 0xFFFFFFFF);
+ }
+};
+
+// We have to use a special object which gets initialized before all other global objects, because those might use the
+// CRT and go kaboom. Yucky, but gets the job done.
+#pragma optimize("", off)
+#pragma warning(disable : 4075) // warning C4075: initializers put in unrecognized initialization area
+#pragma init_seg(".CRT$XCT")
+VCRuntimeCheckObject s_vcruntime_checker;