diff --git a/src/core-tests/CMakeLists.txt b/src/core-tests/CMakeLists.txt
index 7feca55fe..3b02999cf 100644
--- a/src/core-tests/CMakeLists.txt
+++ b/src/core-tests/CMakeLists.txt
@@ -2,23 +2,11 @@
# SPDX-License-Identifier: CC-BY-NC-ND-4.0 + Packaging Restriction
add_executable(core-tests
- ../core/cheats.cpp
- ../core/cpu_disasm.cpp
- ../core/cpu_types.cpp
- ../util/zip_helpers.cpp
cheats_tests.cpp
cpu_disasm_tests.cpp
spu_tests.cpp
- stub_cpu.cpp
+ stub_host.cpp
)
target_include_directories(core-tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
-target_link_libraries(core-tests PRIVATE common imgui libzip::zip gtest gtest_main)
-
-if(APPLE)
- target_compile_options(core-tests PRIVATE -ffunction-sections -fdata-sections)
- target_link_options(core-tests PRIVATE -Wl,-dead_strip)
-elseif(NOT MSVC)
- target_compile_options(core-tests PRIVATE -ffunction-sections -fdata-sections)
- target_link_options(core-tests PRIVATE -Wl,--gc-sections)
-endif()
+target_link_libraries(core-tests PRIVATE core gtest gtest_main)
diff --git a/src/core-tests/core-tests.vcxproj b/src/core-tests/core-tests.vcxproj
index 4b2943e39..4e555f4b0 100644
--- a/src/core-tests/core-tests.vcxproj
+++ b/src/core-tests/core-tests.vcxproj
@@ -3,13 +3,10 @@
-
-
-
-
-
+
+
@@ -21,21 +18,28 @@
{ee054e08-3799-4a59-a422-18259c105ffd}
+
+ {868b98c8-65a1-494b-8346-250a73a48c0a}
+
+
+ {57f6206d-f264-4b07-baf8-11b9bbe1f455}
+
+
+
+
{93D9300C-B64A-4B21-A4B5-30173DE88747}
-
+
%(AdditionalIncludeDirectories);$(SolutionDir)dep\googletest\include
- true
Console
- true
-
+
\ No newline at end of file
diff --git a/src/core-tests/core-tests.vcxproj.filters b/src/core-tests/core-tests.vcxproj.filters
index feaa366ba..fb8c94920 100644
--- a/src/core-tests/core-tests.vcxproj.filters
+++ b/src/core-tests/core-tests.vcxproj.filters
@@ -2,12 +2,12 @@
-
-
-
-
-
+
+
-
+
+
+
+
\ No newline at end of file
diff --git a/src/core-tests/spu_tests.cpp b/src/core-tests/spu_tests.cpp
index a7a7437ae..c95eecc99 100644
--- a/src/core-tests/spu_tests.cpp
+++ b/src/core-tests/spu_tests.cpp
@@ -338,7 +338,7 @@ static ADPCMDecodeResult DecodeADPCMPaired(const std::array& edata, u8 r
s16* output = result.samples.data();
for (u32 i = 0; i < static_cast(edata.size()); i++)
{
- const u8 data = edata.data[i];
+ const u8 data = edata[i];
// extend 4-bit to 16-bit, apply shift from header and mix in previous samples
// this is interleaved and whacky to try to maximize instruction-level parallelism, but basically, it's:
diff --git a/src/core-tests/stub_cpu.cpp b/src/core-tests/stub_cpu.cpp
deleted file mode 100644
index 677ecc375..000000000
--- a/src/core-tests/stub_cpu.cpp
+++ /dev/null
@@ -1,57 +0,0 @@
-// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin
-// SPDX-License-Identifier: CC-BY-NC-ND-4.0
-
-#include "core/cpu_core.h"
-#include "core/system.h"
-
-CPU::State CPU::g_state;
-
-bool CPU::SafeReadMemoryByte(VirtualMemoryAddress addr, u8* value)
-{
- (void)addr;
- *value = 0;
- return false;
-}
-
-bool CPU::SafeReadMemoryHalfWord(VirtualMemoryAddress addr, u16* value)
-{
- (void)addr;
- *value = 0;
- return false;
-}
-
-bool CPU::SafeReadMemoryWord(VirtualMemoryAddress addr, u32* value)
-{
- (void)addr;
- *value = 0;
- return false;
-}
-
-bool CPU::SafeWriteMemoryByte(VirtualMemoryAddress addr, u8 value)
-{
- return false;
-}
-
-bool CPU::SafeWriteMemoryHalfWord(VirtualMemoryAddress addr, u16 value)
-{
- return false;
-}
-
-bool CPU::SafeWriteMemoryWord(VirtualMemoryAddress addr, u32 value)
-{
- return false;
-}
-
-void CPU::InvalidateICacheAt(VirtualMemoryAddress address)
-{
-}
-
-u32 System::GetFrameNumber()
-{
- return 0;
-}
-
-Controller* System::GetController(u32 slot)
-{
- return nullptr;
-}
diff --git a/src/core-tests/stub_host.cpp b/src/core-tests/stub_host.cpp
new file mode 100644
index 000000000..2564a060a
--- /dev/null
+++ b/src/core-tests/stub_host.cpp
@@ -0,0 +1,394 @@
+// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin
+// SPDX-License-Identifier: CC-BY-NC-ND-4.0
+
+#include "core/achievements_private.h"
+#include "core/core.h"
+#include "core/core_private.h"
+#include "core/cpu_core_private.h"
+#include "core/fullscreenui.h"
+#include "core/fullscreenui_widgets.h"
+#include "core/game_list.h"
+#include "core/gdb_server.h"
+#include "core/gpu_backend.h"
+#include "core/host.h"
+#include "core/performance_counters.h"
+#include "core/system_private.h"
+#include "core/video_thread_private.h"
+
+#include "util/http_downloader.h"
+#include "util/imgui_manager.h"
+#include "util/input_manager.h"
+#include "util/translation.h"
+#include "util/window_info.h"
+
+#include "common/log.h"
+#include "common/time_helpers.h"
+
+LOG_CHANNEL(Host);
+
+void Host::OnSettingsReloaded()
+{
+}
+
+void Host::CommitBaseSettingChanges()
+{
+}
+
+bool Host::ResourceFileExists(std::string_view filename, bool allow_override)
+{
+ return false;
+}
+
+std::optional> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)
+{
+ return std::nullopt;
+}
+
+std::optional Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)
+{
+ return std::nullopt;
+}
+
+std::optional Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)
+{
+ return std::nullopt;
+}
+
+void Host::ReportFatalError(std::string_view title, std::string_view message)
+{
+ ERROR_LOG("ReportFatalError: {}", message);
+ abort();
+}
+
+void Host::ReportErrorAsync(std::string_view title, std::string_view message)
+{
+ if (!title.empty() && !message.empty())
+ ERROR_LOG("ReportErrorAsync: {}: {}", title, message);
+ else if (!message.empty())
+ ERROR_LOG("ReportErrorAsync: {}", message);
+}
+
+void Host::ReportStatusMessage(std::string_view message)
+{
+ INFO_LOG("ReportStatusMessage: {}", message);
+}
+
+void Host::ConfirmMessageAsync(std::string_view icon, std::string_view title, std::string_view message,
+ ConfirmMessageAsyncCallback callback, std::string_view yes_text,
+ std::string_view no_text)
+{
+ if (!title.empty() && !message.empty())
+ ERROR_LOG("ConfirmMessage: {}: {}", title, message);
+ else if (!message.empty())
+ ERROR_LOG("ConfirmMessage: {}", message);
+
+ callback(true);
+}
+
+void Host::ReportDebuggerEvent(CPU::DebuggerEvent event, std::string_view message)
+{
+ ERROR_LOG("ReportDebuggerEvent: {}", message);
+}
+
+std::span> Host::GetAvailableLanguageList()
+{
+ return {};
+}
+
+const char* Host::GetLanguageName(std::string_view language_code)
+{
+ return "";
+}
+
+bool Host::ChangeLanguage(const char* new_language)
+{
+ return false;
+}
+
+s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,
+ std::string_view disambiguation, char* tbuf, size_t tbuf_space)
+{
+ if (msg.size() > tbuf_space)
+ return -1;
+ else if (msg.empty())
+ return 0;
+
+ std::memcpy(tbuf, msg.data(), msg.size());
+ return static_cast(msg.size());
+}
+
+std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)
+{
+ TinyString count_str = TinyString::from_format("{}", count);
+
+ std::string ret(msg);
+ for (;;)
+ {
+ std::string::size_type pos = ret.find("%n");
+ if (pos == std::string::npos)
+ break;
+
+ ret.replace(pos, pos + 2, count_str.view());
+ }
+
+ return ret;
+}
+
+TinyString Host::TranslatePluralToTinyString(const char* context, const char* msg, const char* disambiguation,
+ int count)
+{
+ TinyString ret(msg);
+ ret.replace("%n", TinyString::from_format("{}", count));
+ return ret;
+}
+
+SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,
+ int count)
+{
+ SmallString ret(msg);
+ ret.replace("%n", TinyString::from_format("{}", count));
+ return ret;
+}
+
+static TinyString FormatDateOrTime(const char* format, std::time_t timestamp)
+{
+ TinyString ret;
+ if (const std::optional ltime = Common::LocalTime(timestamp))
+ ret.resize(static_cast(std::strftime(ret.data(), ret.buffer_size(), format, <ime.value())));
+ else
+ ret = "Invalid";
+
+ return ret;
+}
+
+TinyString Host::FormatDate(std::time_t timestamp, bool long_format)
+{
+ return FormatDateOrTime(long_format ? "%A %B %e %Y" : "%x", timestamp);
+}
+
+TinyString Host::FormatTime(std::time_t timestamp, bool long_format)
+{
+ return FormatDateOrTime("%X", timestamp);
+}
+
+TinyString Host::FormatDateTime(std::time_t timestamp, bool long_format)
+{
+ return FormatDateOrTime(long_format ? "%c" : "%X %x", timestamp);
+}
+
+void Host::OnSystemStarting()
+{
+}
+
+void Host::OnSystemStarted()
+{
+}
+
+void Host::OnSystemStopping()
+{
+}
+
+void Host::OnSystemDestroyed()
+{
+}
+
+void Host::OnSystemPaused()
+{
+}
+
+void Host::OnSystemResumed()
+{
+}
+
+void Host::OnSystemAbnormalShutdown(const std::string_view reason)
+{
+}
+
+void Host::OnVideoThreadRunIdleChanged(bool is_active)
+{
+}
+
+bool Host::SetScreensaverInhibit(bool inhibit, Error* error)
+{
+ return false;
+}
+
+void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)
+{
+}
+
+void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,
+ const std::string& game_name, GameHash hash)
+{
+}
+
+void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)
+{
+}
+
+void Host::OnMediaCaptureStarted(MediaCaptureMode mode)
+{
+}
+
+void Host::OnMediaCaptureStopped()
+{
+}
+
+void Host::OnHTTPDownloaderActiveChanged(bool active)
+{
+}
+
+void Host::OnGDBServerActiveClientsChanged(bool has_clients)
+{
+}
+
+void Host::PumpMessagesOnCoreThread()
+{
+}
+
+void Host::RunOnCoreThread(std::function function, bool block /* = false */)
+{
+ function();
+}
+
+void Host::RunOnUIThread(std::function function, bool block /* = false */)
+{
+ function();
+}
+
+void Host::RequestResizeHostDisplay(s32 width, s32 height)
+{
+}
+
+void Host::SetDefaultSettings(SettingsInterface& si)
+{
+}
+
+void Host::OnSettingsResetToDefault(bool host, bool system, bool controller)
+{
+}
+
+void Host::RequestExitApplication(bool save_state_if_running)
+{
+}
+
+void Host::RequestExitBigPicture()
+{
+}
+
+void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)
+{
+}
+
+std::optional Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,
+ Error* error)
+{
+ return std::nullopt;
+}
+
+WindowInfoType Host::GetRenderWindowInfoType()
+{
+ return WindowInfoType::Surfaceless;
+}
+
+void Host::ReleaseRenderWindow()
+{
+}
+
+bool Host::CanChangeFullscreenMode(bool new_fullscreen_state)
+{
+ return false;
+}
+
+bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,
+ std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,
+ AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)
+{
+ return false;
+}
+
+void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */,
+ s32* pos_y /* = nullptr */, u32* width /* = nullptr */,
+ u32* height /* = nullptr */)
+{
+}
+
+void Host::FrameDoneOnVideoThread(GPUBackend* gpu_backend, u32 frame_number)
+{
+}
+
+void Host::OpenURL(std::string_view url)
+{
+}
+
+std::string Host::GetClipboardText()
+{
+ return {};
+}
+
+bool Host::CopyTextToClipboard(std::string_view text)
+{
+ return false;
+}
+
+void Host::SetMouseMode(bool relative, bool hide_cursor)
+{
+}
+
+void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)
+{
+}
+
+void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 casual_points, u32 unread_messages)
+{
+}
+
+void Host::OnAchievementsActiveChanged(bool active)
+{
+}
+
+void Host::OnAchievementsHardcoreModeChanged(bool enabled)
+{
+}
+
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+
+void Host::OnRAIntegrationMenuChanged()
+{
+}
+
+#endif
+
+const char* Host::GetDefaultFullscreenUITheme()
+{
+ return "";
+}
+
+void Host::AddFixedInputBindings(const SettingsInterface& si)
+{
+}
+
+void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)
+{
+}
+
+void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)
+{
+}
+
+std::optional Host::GetTopLevelWindowInfo()
+{
+ return std::nullopt;
+}
+
+void Host::RefreshGameListAsync(bool invalidate_cache)
+{
+}
+
+void Host::CancelGameListRefresh()
+{
+}
+
+void Host::OnGameListEntriesChanged(std::span changed_indices)
+{
+}
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 52f5c5d6d..a7786f69f 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -163,7 +163,7 @@ set(RECOMPILER_SRCS
target_precompile_headers(core PRIVATE "pch.h")
target_include_directories(core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
-target_link_libraries(core PUBLIC Threads::Threads common util)
+target_link_libraries(core PUBLIC Threads::Threads common util scmversion)
target_link_libraries(core PRIVATE xxhash imgui rapidyaml rcheevos cpuinfo::cpuinfo ZLIB::ZLIB zstd::libzstd_shared speex_resampler_headers)
if(CPU_ARCH_X64)