From dcab1dd7d0f351e97b7da85d948dbcf98fdaa809 Mon Sep 17 00:00:00 2001 From: Douglas Whittingham Date: Tue, 4 Aug 2026 13:15:00 -1000 Subject: [PATCH 1/2] Include StaticRecompCore.h where the interpreter uses it unknown_instruction reads g_static_recomp_core and ppc_state, but the translation unit includes neither the header that declares the former nor a binding for the latter, so core does not compile: Interpreter.cpp(294): error C2065: 'g_static_recomp_core': undeclared identifier Interpreter.cpp(299): error C2065: 'ppc_state': undeclared identifier It is not reached through the PCH either -- pch.h does not pull in StaticRecompCore.h. The ppc_state binding already existed further down the same function, past the first use. Moving it to the top serves both uses rather than adding a second one, which would be a redefinition. --- Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp b/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp index 4d0bbbfa59..2fa8cde1dc 100644 --- a/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp +++ b/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp @@ -23,6 +23,7 @@ #include "Core/PowerPC/MMU.h" #include "Core/PowerPC/PPCTables.h" #include "Core/PowerPC/PowerPC.h" +#include "Core/PowerPC/StaticRecomp/StaticRecompCore.h" #include "Core/System.h" namespace @@ -287,6 +288,7 @@ void Interpreter::Run() void Interpreter::unknown_instruction(Interpreter& interpreter, UGeckoInstruction inst) { ASSERT(Core::IsCPUThread()); + auto& ppc_state = interpreter.m_ppc_state; auto& system = interpreter.m_system; Core::CPUThreadGuard guard(system); @@ -337,7 +339,6 @@ void Interpreter::unknown_instruction(Interpreter& interpreter, UGeckoInstructio Dolphin_Debugger::PrintCallstack(guard, Common::Log::LogType::POWERPC, Common::Log::LogLevel::LNOTICE); - const auto& ppc_state = interpreter.m_ppc_state; NOTICE_LOG_FMT( POWERPC, "\nIntCPU: Unknown instruction {:08x} at PC = {:08x} last_PC = {:08x} LR = {:08x}\n", From 25a593fb5c5ab9a5e9921f54f37381db3828702e Mon Sep 17 00:00:00 2001 From: Douglas Whittingham Date: Tue, 4 Aug 2026 22:04:50 -1000 Subject: [PATCH 2/2] Give the Win32 render window a File/View menu and hotkeys DolphinNoGUI's window had no way to save or load a state, and Esc as the only key. Getting back to a scene meant booting and playing in. File carries Save State (F1), a Load State submenu, and Pause. View carries Fullscreen (F11 or Alt+Enter) and Mute Audio. Space held is fast-forward. All of it goes through Core, State and Config directly rather than new Host_* hooks, so no frontend has to implement anything to pick this up. States are written to and listed from Dolphin's own StateSaves directory, named by wall clock rather than by slot, so repeated saves accumulate instead of overwriting each other and sort meaningfully by name. The Load State list is rebuilt whenever the menu opens, because states appear while it is closed; it is newest-first with a filename tiebreak so the order does not shuffle between opens on a filesystem with coarse timestamps. F1 ignores auto-repeat -- bit 30 of lParam -- or holding it down would write a state every few milliseconds. Fast-forward is also cancelled on WM_KILLFOCUS, so alt-tabbing mid-hold cannot leave emulation stuck at 2x with the key-up delivered elsewhere. --- Source/Core/DolphinNoGUI/PlatformWin32.cpp | 260 ++++++++++++++++++++- 1 file changed, 258 insertions(+), 2 deletions(-) diff --git a/Source/Core/DolphinNoGUI/PlatformWin32.cpp b/Source/Core/DolphinNoGUI/PlatformWin32.cpp index d2e2f2fba5..168415baae 100644 --- a/Source/Core/DolphinNoGUI/PlatformWin32.cpp +++ b/Source/Core/DolphinNoGUI/PlatformWin32.cpp @@ -6,9 +6,18 @@ #include "Core/Config/MainSettings.h" #include "Core/Config/ConfigManager.h" #include "Core/Core.h" +#include "Core/State.h" #include "Core/System.h" +#include "Common/CommonPaths.h" +#include "Common/FileUtil.h" + +#include #include +#include +#include +#include +#include #include #include #include @@ -19,6 +28,19 @@ namespace { +// Menu command ids. Load State entries are allocated a contiguous range, +// since the list is rebuilt from disk each time the menu opens. +constexpr UINT ID_SAVE_STATE = 41001; +constexpr UINT ID_PAUSE = 41002; +constexpr UINT ID_MUTE = 41003; +constexpr UINT ID_FULLSCREEN = 41004; +constexpr UINT ID_LOAD_STATE_FIRST = 41100; +constexpr UINT ID_LOAD_STATE_LAST = 41199; + +// Hold-to-fast-forward target. 2x is fast enough to skip a cutscene without +// outrunning what most hosts can actually emulate. +constexpr float FAST_FORWARD_SPEED = 2.0f; + class PlatformWin32 final : public Platform { public: @@ -37,10 +59,23 @@ class PlatformWin32 final : public Platform static bool RegisterRenderWindowClass(); bool CreateRenderWindow(); + bool CreateMenus(); + void RefreshMenu(HMENU menu); + void SaveStateToStatesDirectory(); + void ToggleFullscreen(); void UpdateWindowPosition(); void ProcessEvents(); HWND m_hwnd{}; + HMENU m_menu{}; + HMENU m_file_menu{}; + HMENU m_load_menu{}; + HMENU m_view_menu{}; + // Parallel to the Load State menu entries, rebuilt whenever it opens. + std::vector m_load_state_paths; + bool m_fullscreen = false; + LONG m_windowed_style = 0; + RECT m_windowed_rect{}; int m_window_x = Config::Get(Config::MAIN_RENDER_WINDOW_XPOS); int m_window_y = Config::Get(Config::MAIN_RENDER_WINDOW_YPOS); @@ -96,9 +131,147 @@ bool PlatformWin32::CreateRenderWindow() return true; } +bool PlatformWin32::CreateMenus() +{ + m_menu = CreateMenu(); + m_file_menu = CreatePopupMenu(); + m_load_menu = CreatePopupMenu(); + m_view_menu = CreatePopupMenu(); + if (!m_menu || !m_file_menu || !m_load_menu || !m_view_menu) + return false; + + AppendMenuW(m_file_menu, MF_STRING, ID_SAVE_STATE, L"&Save State\tF1"); + AppendMenuW(m_file_menu, MF_POPUP, reinterpret_cast(m_load_menu), L"&Load State"); + AppendMenuW(m_file_menu, MF_SEPARATOR, 0, nullptr); + // Checked state is refreshed from the core when the menu opens, so it cannot + // drift out of step with an emulation that was paused some other way. + AppendMenuW(m_file_menu, MF_STRING, ID_PAUSE, L"&Pause"); + + AppendMenuW(m_view_menu, MF_STRING, ID_FULLSCREEN, L"&Fullscreen\tAlt+Enter"); + AppendMenuW(m_view_menu, MF_STRING, ID_MUTE, L"&Mute Audio"); + + AppendMenuW(m_menu, MF_POPUP, reinterpret_cast(m_file_menu), L"&File"); + AppendMenuW(m_menu, MF_POPUP, reinterpret_cast(m_view_menu), L"&View"); + return SetMenu(m_hwnd, m_menu) != FALSE; +} + +// Rebuilt on open rather than cached: states are written by this process while +// the menu is closed, and by the launcher between sessions. +void PlatformWin32::RefreshMenu(const HMENU menu) +{ + if (menu == m_file_menu) + { + auto& system = Core::System::GetInstance(); + const bool paused = Core::GetState(system) == Core::State::Paused; + CheckMenuItem(m_file_menu, ID_PAUSE, MF_BYCOMMAND | (paused ? MF_CHECKED : MF_UNCHECKED)); + return; + } + + if (menu == m_view_menu) + { + CheckMenuItem(m_view_menu, ID_MUTE, + MF_BYCOMMAND | + (Config::Get(Config::MAIN_AUDIO_MUTED) ? MF_CHECKED : MF_UNCHECKED)); + CheckMenuItem(m_view_menu, ID_FULLSCREEN, + MF_BYCOMMAND | (m_fullscreen ? MF_CHECKED : MF_UNCHECKED)); + return; + } + + if (menu != m_load_menu) + return; + + while (DeleteMenu(m_load_menu, 0, MF_BYPOSITION)) + { + } + m_load_state_paths.clear(); + + const std::string directory = File::GetUserPath(D_STATESAVES_IDX); + std::vector states; + std::error_code ec; + for (const auto& entry : std::filesystem::directory_iterator(directory, ec)) + { + if (entry.is_regular_file(ec) && entry.path().extension() == ".sav") + states.push_back(entry.path()); + } + // Newest first: the state wanted next is nearly always the one just written. + // Filename breaks ties so the order does not shuffle between opens on a + // filesystem whose timestamps are coarse. + std::sort(states.begin(), states.end(), + [](const std::filesystem::path& left, const std::filesystem::path& right) { + std::error_code left_ec; + std::error_code right_ec; + const auto left_time = std::filesystem::last_write_time(left, left_ec); + const auto right_time = std::filesystem::last_write_time(right, right_ec); + if (!left_ec && !right_ec && left_time != right_time) + return left_time > right_time; + return left.filename().string() < right.filename().string(); + }); + + if (states.empty()) + { + AppendMenuW(m_load_menu, MF_STRING | MF_GRAYED, 0, L"(no savestates)"); + return; + } + + const std::size_t limit = std::min( + states.size(), ID_LOAD_STATE_LAST - ID_LOAD_STATE_FIRST + 1); + for (std::size_t i = 0; i < limit; ++i) + { + AppendMenuW(m_load_menu, MF_STRING, ID_LOAD_STATE_FIRST + i, + states[i].filename().wstring().c_str()); + m_load_state_paths.push_back(states[i].string()); + } +} + +void PlatformWin32::SaveStateToStatesDirectory() +{ + // Named by wall-clock rather than by slot, so repeated saves accumulate + // instead of overwriting one another, and sort meaningfully by name. + const std::time_t now = std::time(nullptr); + std::tm local{}; + localtime_s(&local, &now); + char stamp[32]; + std::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &local); + + const std::string directory = File::GetUserPath(D_STATESAVES_IDX); + File::CreateFullPath(directory); + State::SaveAs(Core::System::GetInstance(), directory + "state-" + stamp + ".sav"); +} + +void PlatformWin32::ToggleFullscreen() +{ + if (!m_fullscreen) + { + GetWindowRect(m_hwnd, &m_windowed_rect); + m_windowed_style = GetWindowLong(m_hwnd, GWL_STYLE); + + MONITORINFO monitor{}; + monitor.cbSize = sizeof(monitor); + if (!GetMonitorInfo(MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST), &monitor)) + return; + + SetMenu(m_hwnd, nullptr); + SetWindowLong(m_hwnd, GWL_STYLE, m_windowed_style & ~WS_OVERLAPPEDWINDOW); + SetWindowPos(m_hwnd, HWND_TOP, monitor.rcMonitor.left, monitor.rcMonitor.top, + monitor.rcMonitor.right - monitor.rcMonitor.left, + monitor.rcMonitor.bottom - monitor.rcMonitor.top, + SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + m_fullscreen = true; + return; + } + + SetWindowLong(m_hwnd, GWL_STYLE, m_windowed_style); + SetMenu(m_hwnd, m_menu); + SetWindowPos(m_hwnd, nullptr, m_windowed_rect.left, m_windowed_rect.top, + m_windowed_rect.right - m_windowed_rect.left, + m_windowed_rect.bottom - m_windowed_rect.top, + SWP_NOOWNERZORDER | SWP_FRAMECHANGED | SWP_NOZORDER); + m_fullscreen = false; +} + bool PlatformWin32::Init() { - if (!RegisterRenderWindowClass() || !CreateRenderWindow()) + if (!RegisterRenderWindowClass() || !CreateRenderWindow() || !CreateMenus()) return false; // TODO: Enter fullscreen if enabled. @@ -200,10 +373,93 @@ LRESULT PlatformWin32::WndProc(const HWND hwnd, const UINT msg, const WPARAM wPa break; case WM_KEYDOWN: - if (wParam == VK_ESCAPE) + // Bit 30 of lParam is the previous key state: ignore auto-repeat, or holding + // F1 down would write a state every few milliseconds. + if (wParam == VK_F1 && (static_cast(lParam) & (1u << 30)) == 0) + { + platform->SaveStateToStatesDirectory(); + return 0; + } + else if (wParam == VK_SPACE) + { + Config::SetCurrent(Config::MAIN_EMULATION_SPEED, FAST_FORWARD_SPEED); + return 0; + } + else if (wParam == VK_F11) + { + platform->ToggleFullscreen(); + return 0; + } + else if (wParam == VK_ESCAPE && platform->m_fullscreen) + { + platform->ToggleFullscreen(); + return 0; + } + else if (wParam == VK_ESCAPE) + { platform->RequestShutdown(); + } + break; + + case WM_KEYUP: + if (wParam == VK_SPACE) + { + Config::SetCurrent(Config::MAIN_EMULATION_SPEED, 1.0f); + return 0; + } break; + case WM_KILLFOCUS: + // Never leave emulation running fast because Space was released while + // another window had focus and the key-up went elsewhere. + Config::SetCurrent(Config::MAIN_EMULATION_SPEED, 1.0f); + break; + + case WM_SYSKEYDOWN: + if (wParam == VK_RETURN && (GetKeyState(VK_MENU) & 0x8000) != 0) + { + platform->ToggleFullscreen(); + return 0; + } + return DefWindowProc(hwnd, msg, wParam, lParam); + + case WM_INITMENUPOPUP: + if (platform) + platform->RefreshMenu(reinterpret_cast(wParam)); + break; + + case WM_COMMAND: + { + if (!platform) + break; + const UINT command = LOWORD(wParam); + auto& system = Core::System::GetInstance(); + if (command == ID_SAVE_STATE) + { + platform->SaveStateToStatesDirectory(); + } + else if (command == ID_PAUSE) + { + const bool paused = Core::GetState(system) == Core::State::Paused; + Core::SetState(system, paused ? Core::State::Running : Core::State::Paused); + } + else if (command == ID_MUTE) + { + Config::SetCurrent(Config::MAIN_AUDIO_MUTED, !Config::Get(Config::MAIN_AUDIO_MUTED)); + } + else if (command == ID_FULLSCREEN) + { + platform->ToggleFullscreen(); + } + else if (command >= ID_LOAD_STATE_FIRST && command <= ID_LOAD_STATE_LAST) + { + const std::size_t index = command - ID_LOAD_STATE_FIRST; + if (index < platform->m_load_state_paths.size()) + State::LoadAs(system, platform->m_load_state_paths[index]); + } + break; + } + case WM_CLOSE: platform->RequestShutdown(); break;