From ffef0c2e383613496b1afcc1aaa2ab36e7346a16 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 14:10:55 +1000 Subject: [PATCH 01/22] CPU/CodeCache: Don't compile invalid jumps via block links --- src/core/cpu_code_cache.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/cpu_code_cache.cpp b/src/core/cpu_code_cache.cpp index dcd8c68e36..f85c6f62b0 100644 --- a/src/core/cpu_code_cache.cpp +++ b/src/core/cpu_code_cache.cpp @@ -61,6 +61,7 @@ static void ClearBlocks(); static Block* LookupBlock(u32 pc); static Block* CreateBlock(u32 pc, const BlockInstructionList& instructions, const BlockMetadata& metadata); +static bool HasBlockLUT(u32 pc); static bool IsBlockCodeCurrent(const Block* block); static bool RevalidateBlock(Block* block); static PageProtectionMode GetProtectionModeForPC(u32 pc); @@ -362,6 +363,12 @@ CPU::CodeCache::Block* CPU::CodeCache::LookupBlock(u32 pc) return s_block_lut[table][idx]; } +bool CPU::CodeCache::HasBlockLUT(u32 pc) +{ + const u32 table = pc >> LUT_TABLE_SHIFT; + return (s_block_lut[table] != nullptr); +} + CPU::CodeCache::Block* CPU::CodeCache::CreateBlock(u32 pc, const BlockInstructionList& instructions, const BlockMetadata& metadata) { @@ -1372,7 +1379,7 @@ const void* CPU::CodeCache::CreateBlockLink(Block* block, void* code, u32 newpc) } else { - dst = g_compile_or_revalidate_block; + dst = HasBlockLUT(newpc) ? g_compile_or_revalidate_block : g_interpret_block; } BlockLinkMap::iterator iter = s_block_links.emplace(newpc, code); From 884459d1cfd96771e4620abd355b864ceefa0540 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 14:25:56 +1000 Subject: [PATCH 02/22] FullscreenUI: Fade alpha change when switching to postfx settings --- src/core/fullscreen_ui.cpp | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 954bc80a92..1d6c7b0375 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -280,6 +280,7 @@ static void DrawAdvancedSettingsPage(); static void DrawPatchesOrCheatsSettingsPage(bool cheats); static bool ShouldShowAdvancedSettings(); +static float GetSettingsWindowBgAlpha(); static bool IsEditingGameSettings(SettingsInterface* bsi); static SettingsInterface* GetEditingSettingsInterface(); static SettingsInterface* GetEditingSettingsInterface(bool game_settings); @@ -442,6 +443,7 @@ struct ALIGN_TO_CACHE_LINE UIState std::shared_ptr fallback_playlist_texture; // Settings + float settings_last_bg_alpha = 1.0f; SettingsPage settings_page = SettingsPage::Interface; std::unique_ptr game_settings_interface; const GameDatabase::Entry* game_settings_db_entry; @@ -1597,6 +1599,11 @@ bool FullscreenUI::ShouldShowAdvancedSettings() return Host::GetBaseBoolSettingValue("Main", "ShowDebugMenu", false); } +float FullscreenUI::GetSettingsWindowBgAlpha() +{ + return GPUThread::HasGPUBackend() ? (s_state.settings_page == SettingsPage::PostProcessing ? 0.50f : 0.90f) : 1.0f; +} + bool FullscreenUI::IsEditingGameSettings(SettingsInterface* bsi) { return (bsi == s_state.game_settings_interface.get()); @@ -2798,6 +2805,7 @@ void FullscreenUI::SwitchToSettings() s_state.current_main_window = MainWindowType::Settings; s_state.settings_page = SettingsPage::Interface; + s_state.settings_last_bg_alpha = GetSettingsWindowBgAlpha(); } void FullscreenUI::SwitchToGameSettingsForSerial(std::string_view serial) @@ -2917,11 +2925,14 @@ void FullscreenUI::DrawSettingsWindow() ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f)); - const float bg_alpha = - GPUThread::HasGPUBackend() ? (s_state.settings_page == SettingsPage::PostProcessing ? 0.50f : 0.90f) : 1.0f; + const float target_bg_alpha = GetSettingsWindowBgAlpha(); + s_state.settings_last_bg_alpha = (target_bg_alpha < s_state.settings_last_bg_alpha) ? + std::max(s_state.settings_last_bg_alpha - io.DeltaTime * 2.0f, target_bg_alpha) : + std::min(s_state.settings_last_bg_alpha + io.DeltaTime * 2.0f, target_bg_alpha); - if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "settings_category", - ImVec4(UIStyle.PrimaryColor.x, UIStyle.PrimaryColor.y, UIStyle.PrimaryColor.z, bg_alpha))) + if (BeginFullscreenWindow( + ImVec2(0.0f, 0.0f), heading_size, "settings_category", + ImVec4(UIStyle.PrimaryColor.x, UIStyle.PrimaryColor.y, UIStyle.PrimaryColor.z, s_state.settings_last_bg_alpha))) { static constexpr float ITEM_WIDTH = 25.0f; @@ -3020,8 +3031,9 @@ void FullscreenUI::DrawSettingsWindow() ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), TinyString::from_format("settings_page_{}", static_cast(s_state.settings_page)).c_str(), - ImVec4(UIStyle.BackgroundColor.x, UIStyle.BackgroundColor.y, UIStyle.BackgroundColor.z, bg_alpha), 0.0f, - ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f))) + ImVec4(UIStyle.BackgroundColor.x, UIStyle.BackgroundColor.y, UIStyle.BackgroundColor.z, + s_state.settings_last_bg_alpha), + 0.0f, ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f))) { ResetFocusHere(); From 57be62ffd12fd5ab77f3cca4b211b9a6a7944a18 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 14:27:13 +1000 Subject: [PATCH 03/22] FullscreenUI: Remove a couple of untranslated titles --- src/core/fullscreen_ui.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 1d6c7b0375..5ac2317420 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -2899,8 +2899,8 @@ void FullscreenUI::DoCopyGameSettings() temp_settings.Save(*s_state.game_settings_interface, true); SetSettingsChanged(s_state.game_settings_interface.get()); - ShowToast("Game Settings Copied", fmt::format(FSUI_FSTR("Game settings initialized with global settings for '{}'."), - Path::GetFileTitle(s_state.game_settings_interface->GetPath()))); + ShowToast(std::string(), fmt::format(FSUI_FSTR("Game settings initialized with global settings for '{}'."), + Path::GetFileTitle(s_state.game_settings_interface->GetPath()))); } void FullscreenUI::DoClearGameSettings() @@ -2914,8 +2914,8 @@ void FullscreenUI::DoClearGameSettings() SetSettingsChanged(s_state.game_settings_interface.get()); - ShowToast("Game Settings Cleared", fmt::format(FSUI_FSTR("Game settings have been cleared for '{}'."), - Path::GetFileTitle(s_state.game_settings_interface->GetPath()))); + ShowToast(std::string(), fmt::format(FSUI_FSTR("Game settings have been cleared for '{}'."), + Path::GetFileTitle(s_state.game_settings_interface->GetPath()))); } void FullscreenUI::DrawSettingsWindow() @@ -7834,7 +7834,7 @@ void FullscreenUI::CloseLoadingScreen() ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Translation String Area -// To avoid having to type T_RANSLATE("FullscreenUI", ...) everywhere, we use the shorter macros at the top +// To avoid having to type TRANSLATE("FullscreenUI", ...) everywhere, we use the shorter macros at the top // of the file, then preprocess and generate a bunch of noops here to define the strings. Sadly that means // the view in Linguist is gonna suck, but you can search the file for the string for more context. ///////////////////////////////////////////////////////////////////////////////////////////////////////////// From 8605722cdfc146ee6a6e5b0d0d13030f898fb124 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 17:15:17 +1000 Subject: [PATCH 04/22] GameDB: GTA does not support analog mode --- data/resources/gamedb.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/data/resources/gamedb.yaml b/data/resources/gamedb.yaml index 6dd2b6965f..0a060ac282 100644 --- a/data/resources/gamedb.yaml +++ b/data/resources/gamedb.yaml @@ -67726,7 +67726,6 @@ SLES-00032: - SLES-00032 - SLES-03389 controllers: - - AnalogController - DigitalController metadata: publisher: "Rockstar Games" @@ -67757,7 +67756,6 @@ SLUS-00106: rating: NoIssues versionTested: "0.1-1308-g622e50fa" controllers: - - AnalogController - DigitalController metadata: publisher: "Take 2 Interactive, Inc" From a08acdb93a083f56f91ee357281b48edf5e2a042 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 19:24:15 +1000 Subject: [PATCH 05/22] System: Improve texture recycling when changing rewind/runahead settings Fix suprious failures when changing rewind settings when low on VRAM. --- src/core/gpu_backend.cpp | 23 +++++++++--- src/core/hotkeys.cpp | 8 ++-- src/core/system.cpp | 79 +++++++++++++-------------------------- src/core/system.h | 2 +- src/core/system_private.h | 5 +-- 5 files changed, 51 insertions(+), 66 deletions(-) diff --git a/src/core/gpu_backend.cpp b/src/core/gpu_backend.cpp index d2c2e9b340..e93f05736a 100644 --- a/src/core/gpu_backend.cpp +++ b/src/core/gpu_backend.cpp @@ -332,21 +332,32 @@ bool GPUBackend::AllocateMemorySaveStates(std::span sta for (size_t i = 0; i < states.size(); i++) g_gpu_device->RecycleTexture(std::move(states[i].vram_texture)); + // Maximize potential for texture reuse by flushing the current command buffer. + g_gpu_device->WaitForGPUIdle(); + for (size_t i = 0; i < states.size(); i++) { if (!backend->AllocateMemorySaveState(states[i], error)) { - // Free anything that was allocated. - for (size_t j = 0; j <= i; i++) + // Try flushing the pool. + WARNING_LOG("Failed to allocate memory save state texture, trying flushing pool."); + g_gpu_device->PurgeTexturePool(); + g_gpu_device->WaitForGPUIdle(); + if (!backend->AllocateMemorySaveState(states[i], error)) { - states[j].state_data.deallocate(); - states[j].vram_texture.reset(); - result = false; - return; + // Free anything that was allocated. + for (size_t j = 0; j <= i; i++) + { + states[j].state_data.deallocate(); + states[j].vram_texture.reset(); + result = false; + return; + } } } } + backend->RestoreDeviceContext(); result = true; }, true, false); diff --git a/src/core/hotkeys.cpp b/src/core/hotkeys.cpp index 865aa2ce85..3f4377e59c 100644 --- a/src/core/hotkeys.cpp +++ b/src/core/hotkeys.cpp @@ -59,7 +59,7 @@ static void HotkeyModifyResolutionScale(s32 increment) if (System::IsValid()) { - System::ClearMemorySaveStates(true); + System::ClearMemorySaveStates(true, false); GPUThread::UpdateSettings(true, false); } } @@ -373,7 +373,7 @@ DEFINE_HOTKEY("TogglePGXP", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOO [](s32 pressed) { if (!pressed && System::IsValid()) { - System::ClearMemorySaveStates(true); + System::ClearMemorySaveStates(true, true); g_settings.gpu_pgxp_enable = !g_settings.gpu_pgxp_enable; GPUThread::UpdateSettings(true, false); @@ -451,7 +451,7 @@ DEFINE_HOTKEY("TogglePGXPDepth", TRANSLATE_NOOP("Hotkeys", "Graphics"), if (!g_settings.gpu_pgxp_enable) return; - System::ClearMemorySaveStates(true); + System::ClearMemorySaveStates(true, true); g_settings.gpu_pgxp_depth_buffer = !g_settings.gpu_pgxp_depth_buffer; GPUThread::UpdateSettings(true, false); @@ -471,7 +471,7 @@ DEFINE_HOTKEY("TogglePGXPCPU", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_ if (!g_settings.gpu_pgxp_enable) return; - System::ClearMemorySaveStates(true); + System::ClearMemorySaveStates(true, true); // GPU thread is unchanged g_settings.gpu_pgxp_cpu = !g_settings.gpu_pgxp_cpu; diff --git a/src/core/system.cpp b/src/core/system.cpp index 5a4dc733d3..270f6e89d2 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1167,7 +1167,7 @@ DiscRegion System::GetRegionForPsf(const char* path) void System::RecreateGPU(GPURenderer renderer) { - FreeMemoryStateTextures(); + FreeMemoryStateStorage(false); StopMediaCapture(); Error error; @@ -1177,7 +1177,7 @@ void System::RecreateGPU(GPURenderer renderer) Panic("Failed to switch renderer."); } - ClearMemorySaveStates(true); + ClearMemorySaveStates(true, false); } void System::LoadSettings(bool display_osd_messages) @@ -1954,7 +1954,7 @@ void System::DestroySystem() if (g_settings.inhibit_screensaver) PlatformMisc::ResumeScreensaver(); - FreeMemoryStateStorage(); + FreeMemoryStateStorage(false); Cheats::UnloadAll(); PCDrv::Shutdown(); @@ -2495,13 +2495,13 @@ System::MemorySaveState& System::PopMemoryState() return s_state.memory_save_states[s_state.memory_save_state_front]; } -bool System::AllocateMemoryStates(size_t state_count) +bool System::AllocateMemoryStates(size_t state_count, bool recycle_old_textures) { DEV_LOG("Allocating {} memory save state slots", state_count); if (state_count != s_state.memory_save_states.size()) { - FreeMemoryStateStorage(); + FreeMemoryStateStorage(recycle_old_textures); s_state.memory_save_states.resize(state_count); } @@ -2522,7 +2522,7 @@ bool System::AllocateMemoryStates(size_t state_count) ERROR_LOG("Failed to allocate {} memory save states: {}", s_state.memory_save_states.size(), error.GetDescription()); ERROR_LOG("Disabling runahead/rewind."); - FreeMemoryStateStorage(); + FreeMemoryStateStorage(false); s_state.runahead_frames = 0; s_state.memory_save_state_front = 0; s_state.memory_save_state_count = 0; @@ -2536,48 +2536,16 @@ bool System::AllocateMemoryStates(size_t state_count) return true; } -void System::ClearMemorySaveStates(bool reallocate_resources) +void System::ClearMemorySaveStates(bool reallocate_resources, bool recycle_textures) { s_state.memory_save_state_front = 0; s_state.memory_save_state_count = 0; if (reallocate_resources && !s_state.memory_save_states.empty()) - AllocateMemoryStates(s_state.memory_save_states.size()); + AllocateMemoryStates(s_state.memory_save_states.size(), recycle_textures); } -void System::FreeMemoryStateTextures() -{ - // TODO: use non-copyable function, that way we don't need to store raw pointers - std::vector textures; - bool gpu_thread_synced = false; - - for (MemorySaveState& mss : s_state.memory_save_states) - { - if ((mss.vram_texture || !mss.gpu_state_data.empty()) && !gpu_thread_synced) - { - gpu_thread_synced = true; - GPUThread::SyncGPUThread(true); - } - - if (mss.vram_texture) - { - if (textures.empty()) - textures.reserve(s_state.memory_save_states.size()); - - textures.push_back(mss.vram_texture.release()); - } - } - - if (!textures.empty()) - { - GPUThread::RunOnThread([textures = std::move(textures)]() mutable { - for (GPUTexture* texture : textures) - g_gpu_device->RecycleTexture(std::unique_ptr(texture)); - }); - } -} - -void System::FreeMemoryStateStorage() +void System::FreeMemoryStateStorage(bool recycle_textures) { // TODO: use non-copyable function, that way we don't need to store raw pointers std::vector textures; @@ -2607,9 +2575,14 @@ void System::FreeMemoryStateStorage() if (!textures.empty()) { - GPUThread::RunOnThread([textures = std::move(textures)]() mutable { + GPUThread::RunOnThread([textures = std::move(textures), recycle_textures]() mutable { for (GPUTexture* texture : textures) - g_gpu_device->RecycleTexture(std::unique_ptr(texture)); + { + if (recycle_textures) + g_gpu_device->RecycleTexture(std::unique_ptr(texture)); + else + delete texture; + } }); } @@ -2919,7 +2892,7 @@ bool System::LoadStateFromBuffer(const SaveStateBuffer& buffer, Error* error, bo if (g_settings.HasAnyPerGameMemoryCards()) UpdatePerGameMemoryCards(); - ClearMemorySaveStates(false); + ClearMemorySaveStates(false, false); // Updating game/loading settings can turn on hardcore mode. Catch this. Achievements::DisableHardcoreMode(); @@ -4005,7 +3978,7 @@ bool System::InsertMedia(const char* path) if (IsGPUDumpPath(path)) [[unlikely]] return ChangeGPUDump(path); - ClearMemorySaveStates(true); + ClearMemorySaveStates(true, true); Error error; std::unique_ptr image = CDImage::Open(path, g_settings.cdrom_load_image_patches, &error); @@ -4044,7 +4017,7 @@ bool System::InsertMedia(const char* path) void System::RemoveMedia() { - ClearMemorySaveStates(true); + ClearMemorySaveStates(true, true); CDROM::RemoveMedia(false); } @@ -4254,7 +4227,7 @@ bool System::SwitchMediaSubImage(u32 index) if (!CDROM::HasMedia()) return false; - ClearMemorySaveStates(true); + ClearMemorySaveStates(true, true); std::unique_ptr image = CDROM::RemoveMedia(true); Assert(image); @@ -4308,7 +4281,7 @@ void System::CheckForSettingsChanges(const Settings& old_settings) { if (IsValid()) { - ClearMemorySaveStates(false); + ClearMemorySaveStates(false, false); if (g_settings.cpu_overclock_active != old_settings.cpu_overclock_active || (g_settings.cpu_overclock_active && @@ -4448,7 +4421,8 @@ void System::CheckForSettingsChanges(const Settings& old_settings) GPUThread::UpdateSettings(true, false); // NOTE: Must come after the GPU thread settings update, otherwise it allocs the wrong size textures. - ClearMemorySaveStates(true); + const bool use_existing_textures = (g_settings.gpu_resolution_scale == old_settings.gpu_resolution_scale); + ClearMemorySaveStates(true, use_existing_textures); if (IsPaused()) GPUThread::PresentCurrentFrame(); @@ -4884,7 +4858,8 @@ void System::CalculateRewindMemoryUsage(u32 num_saves, u32 resolution_scale, u64 void System::UpdateMemorySaveStateSettings() { - FreeMemoryStateStorage(); + const bool any_memory_states_active = (g_settings.IsRunaheadEnabled() || g_settings.rewind_enable); + FreeMemoryStateStorage(any_memory_states_active); if (IsReplayingGPUDump()) [[unlikely]] { @@ -4926,7 +4901,7 @@ void System::UpdateMemorySaveStateSettings() // allocate storage for memory save states if (num_slots > 0) - AllocateMemoryStates(num_slots); + AllocateMemoryStates(num_slots, true); // reenter execution loop, don't want to try to save a state now if runahead was turned off InterruptExecution(); @@ -5024,7 +4999,7 @@ bool System::DoRunahead() s_state.runahead_replay_frames = s_state.memory_save_state_count; // and throw away all the states, forcing us to catch up below - ClearMemorySaveStates(false); + ClearMemorySaveStates(false, false); // run the frames with no audio SPU::SetAudioOutputMuted(true); diff --git a/src/core/system.h b/src/core/system.h index 33d7b02a53..80e4d116e1 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -418,7 +418,7 @@ void RequestDisplaySize(float scale = 0.0f); // Memory Save States (Rewind and Runahead) ////////////////////////////////////////////////////////////////////////// void CalculateRewindMemoryUsage(u32 num_saves, u32 resolution_scale, u64* ram_usage, u64* vram_usage); -void ClearMemorySaveStates(bool reallocate_resources); +void ClearMemorySaveStates(bool reallocate_resources, bool recycle_textures); void SetRunaheadReplayFlag(); /// Shared socket multiplexer, used by PINE/GDB/etc. diff --git a/src/core/system_private.h b/src/core/system_private.h index 046bec2455..74b874a228 100644 --- a/src/core/system_private.h +++ b/src/core/system_private.h @@ -26,9 +26,8 @@ struct MemorySaveState MemorySaveState& AllocateMemoryState(); MemorySaveState& GetFirstMemoryState(); MemorySaveState& PopMemoryState(); -bool AllocateMemoryStates(size_t state_count); -void FreeMemoryStateTextures(); -void FreeMemoryStateStorage(); +bool AllocateMemoryStates(size_t state_count, bool recycle_old_textures); +void FreeMemoryStateStorage(bool recycle_texture); void LoadMemoryState(MemorySaveState& mss, bool update_display); void SaveMemoryState(MemorySaveState& mss); From f51dda3e665058cea271f91f3c113451fbd8f7a3 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 19:38:46 +1000 Subject: [PATCH 06/22] GPUDevice: Allow this-frame pooled textures when not uploading data It won't break the render pass. --- src/util/gpu_device.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/gpu_device.cpp b/src/util/gpu_device.cpp index 658b0396bd..a18fe0571b 100644 --- a/src/util/gpu_device.cpp +++ b/src/util/gpu_device.cpp @@ -1028,7 +1028,7 @@ std::unique_ptr GPUDevice::FetchTexture(u32 width, u32 height, u32 l TexturePool::iterator it; - if (is_texture && m_features.prefer_unused_textures) + if (is_texture && data && m_features.prefer_unused_textures) { // Try to find a texture that wasn't used this frame first. for (it = m_texture_pool.begin(); it != m_texture_pool.end(); ++it) From d0e1efb1fd0d2249ca8e78b3b14b277ae54e2375 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 21:28:16 +1000 Subject: [PATCH 07/22] Common: Add more GSMatrix ops --- src/common/gsvector.cpp | 129 +++++++++++++++++++++++++++++++++------- src/common/gsvector.h | 4 ++ 2 files changed, 113 insertions(+), 20 deletions(-) diff --git a/src/common/gsvector.cpp b/src/common/gsvector.cpp index 787fb7d668..c62da4d8a9 100644 --- a/src/common/gsvector.cpp +++ b/src/common/gsvector.cpp @@ -87,25 +87,25 @@ GSMatrix4x4::GSMatrix4x4(float e00, float e01, float e02, float e03, float e10, E[3][3] = e33; } - GSMatrix4x4::GSMatrix4x4(const GSMatrix2x2& m) -{ - E[0][0] = m.E[0][0]; - E[0][1] = m.E[0][1]; - E[0][2] = 0.0f; - E[0][3] = 0.0f; - E[1][0] = m.E[1][0]; - E[1][1] = m.E[1][1]; - E[1][2] = 0.0f; - E[1][3] = 0.0f; - E[2][0] = 0.0f; - E[2][1] = 0.0f; - E[2][2] = 1.0f; - E[2][3] = 0.0f; - E[3][0] = 0.0f; - E[3][1] = 0.0f; - E[3][2] = 0.0f; - E[3][3] = 1.0f; - } +GSMatrix4x4::GSMatrix4x4(const GSMatrix2x2& m) +{ + E[0][0] = m.E[0][0]; + E[0][1] = m.E[0][1]; + E[0][2] = 0.0f; + E[0][3] = 0.0f; + E[1][0] = m.E[1][0]; + E[1][1] = m.E[1][1]; + E[1][2] = 0.0f; + E[1][3] = 0.0f; + E[2][0] = 0.0f; + E[2][1] = 0.0f; + E[2][2] = 1.0f; + E[2][3] = 0.0f; + E[3][0] = 0.0f; + E[3][1] = 0.0f; + E[3][2] = 0.0f; + E[3][3] = 1.0f; +} GSMatrix4x4 GSMatrix4x4::operator*(const GSMatrix4x4& m) const { @@ -136,12 +136,41 @@ GSMatrix4x4 GSMatrix4x4::operator*(const GSMatrix4x4& m) const return res; } +GSMatrix4x4& GSMatrix4x4::operator*=(const GSMatrix4x4& m) +{ + const GSMatrix4x4 copy(*this); + +#define MultRC(rw, cl) \ + copy.E[rw][0] * m.E[0][cl] + copy.E[rw][1] * m.E[1][cl] + copy.E[rw][2] * m.E[2][cl] + copy.E[rw][3] * m.E[3][cl] + + E[0][0] = MultRC(0, 0); + E[0][1] = MultRC(0, 1); + E[0][2] = MultRC(0, 2); + E[0][3] = MultRC(0, 3); + E[1][0] = MultRC(1, 0); + E[1][1] = MultRC(1, 1); + E[1][2] = MultRC(1, 2); + E[1][3] = MultRC(1, 3); + E[2][0] = MultRC(2, 0); + E[2][1] = MultRC(2, 1); + E[2][2] = MultRC(2, 2); + E[2][3] = MultRC(2, 3); + E[3][0] = MultRC(3, 0); + E[3][1] = MultRC(3, 1); + E[3][2] = MultRC(3, 2); + E[3][3] = MultRC(3, 3); + +#undef MultRC + + return *this; +} + GSVector4 GSMatrix4x4::operator*(const GSVector4& v) const { const GSVector4 r0 = row(0); const GSVector4 r1 = row(1); const GSVector4 r2 = row(2); - const GSVector4 r3 = row(4); + const GSVector4 r3 = row(3); return GSVector4(r0.dot(v), r1.dot(v), r2.dot(v), r3.dot(v)); } @@ -199,6 +228,11 @@ GSMatrix4x4 GSMatrix4x4::RotationZ(float angle_in_radians) 0.0f, 0.0f, 1.0f); } +GSMatrix4x4 GSMatrix4x4::Translation(float x, float y, float z) +{ + return GSMatrix4x4(1.0f, 0.0f, 0.0f, x, 0.0f, 1.0f, 0.0f, y, 0.0f, 0.0f, 1.0f, z, 0.0f, 0.0f, 0.0f, 1.0f); +} + GSMatrix4x4 GSMatrix4x4::OffCenterOrthographicProjection(float left, float top, float right, float bottom, float zNear, float zFar) { @@ -222,6 +256,61 @@ GSVector4 GSMatrix4x4::col(size_t i) const return GSVector4(E[0][i], E[1][i], E[2][i], E[3][i]); } +GSMatrix4x4 GSMatrix4x4::Invert() const +{ + GSMatrix4x4 ret; + + float v0 = E[2][0] * E[3][1] - E[2][1] * E[3][0]; + float v1 = E[2][0] * E[3][2] - E[2][2] * E[3][0]; + float v2 = E[2][0] * E[3][3] - E[2][3] * E[3][0]; + float v3 = E[2][1] * E[3][2] - E[2][2] * E[3][1]; + float v4 = E[2][1] * E[3][3] - E[2][3] * E[3][1]; + float v5 = E[2][2] * E[3][3] - E[2][3] * E[3][2]; + + const float t00 = +(v5 * E[1][1] - v4 * E[1][2] + v3 * E[1][3]); + const float t10 = -(v5 * E[1][0] - v2 * E[1][2] + v1 * E[1][3]); + const float t20 = +(v4 * E[1][0] - v2 * E[1][1] + v0 * E[1][3]); + const float t30 = -(v3 * E[1][0] - v1 * E[1][1] + v0 * E[1][2]); + + const float inv_det = 1.0f / (t00 * E[0][0] + t10 * E[0][1] + t20 * E[0][2] + t30 * E[0][3]); + + ret.E[0][0] = t00 * inv_det; + ret.E[1][0] = t10 * inv_det; + ret.E[2][0] = t20 * inv_det; + ret.E[3][0] = t30 * inv_det; + + ret.E[0][1] = -(v5 * E[0][1] - v4 * E[0][2] + v3 * E[0][3]) * inv_det; + ret.E[1][1] = +(v5 * E[0][0] - v2 * E[0][2] + v1 * E[0][3]) * inv_det; + ret.E[2][1] = -(v4 * E[0][0] - v2 * E[0][1] + v0 * E[0][3]) * inv_det; + ret.E[3][1] = +(v3 * E[0][0] - v1 * E[0][1] + v0 * E[0][2]) * inv_det; + + v0 = E[1][0] * E[3][1] - E[1][1] * E[3][0]; + v1 = E[1][0] * E[3][2] - E[1][2] * E[3][0]; + v2 = E[1][0] * E[3][3] - E[1][3] * E[3][0]; + v3 = E[1][1] * E[3][2] - E[1][2] * E[3][1]; + v4 = E[1][1] * E[3][3] - E[1][3] * E[3][1]; + v5 = E[1][2] * E[3][3] - E[1][3] * E[3][2]; + + ret.E[0][2] = +(v5 * E[0][1] - v4 * E[0][2] + v3 * E[0][3]) * inv_det; + ret.E[1][2] = -(v5 * E[0][0] - v2 * E[0][2] + v1 * E[0][3]) * inv_det; + ret.E[2][2] = +(v4 * E[0][0] - v2 * E[0][1] + v0 * E[0][3]) * inv_det; + ret.E[3][2] = -(v3 * E[0][0] - v1 * E[0][1] + v0 * E[0][2]) * inv_det; + + v0 = E[2][1] * E[1][0] - E[2][0] * E[1][1]; + v1 = E[2][2] * E[1][0] - E[2][0] * E[1][2]; + v2 = E[2][3] * E[1][0] - E[2][0] * E[1][3]; + v3 = E[2][2] * E[1][1] - E[2][1] * E[1][2]; + v4 = E[2][3] * E[1][1] - E[2][1] * E[1][3]; + v5 = E[2][3] * E[1][2] - E[2][2] * E[1][3]; + + ret.E[0][3] = -(v5 * E[0][1] - v4 * E[0][2] + v3 * E[0][3]) * inv_det; + ret.E[1][3] = +(v5 * E[0][0] - v2 * E[0][2] + v1 * E[0][3]) * inv_det; + ret.E[2][3] = -(v4 * E[0][0] - v2 * E[0][1] + v0 * E[0][3]) * inv_det; + ret.E[3][3] = +(v3 * E[0][0] - v1 * E[0][1] + v0 * E[0][2]) * inv_det; + + return ret; +} + void GSMatrix4x4::store(void* m) { std::memcpy(m, &E[0][0], sizeof(E)); diff --git a/src/common/gsvector.h b/src/common/gsvector.h index 106e29b357..4a3b1306c5 100644 --- a/src/common/gsvector.h +++ b/src/common/gsvector.h @@ -47,6 +47,7 @@ class alignas(VECTOR_ALIGNMENT) GSMatrix4x4 GSMatrix4x4(const GSMatrix2x2& m); GSMatrix4x4 operator*(const GSMatrix4x4& m) const; + GSMatrix4x4& operator*=(const GSMatrix4x4& m); GSVector4 operator*(const GSVector4& v) const; @@ -55,6 +56,7 @@ class alignas(VECTOR_ALIGNMENT) GSMatrix4x4 static GSMatrix4x4 RotationX(float angle_in_radians); static GSMatrix4x4 RotationY(float angle_in_radians); static GSMatrix4x4 RotationZ(float angle_in_radians); + static GSMatrix4x4 Translation(float x, float y, float z); static GSMatrix4x4 OffCenterOrthographicProjection(float left, float top, float right, float bottom, float zNear, float zFar); @@ -63,6 +65,8 @@ class alignas(VECTOR_ALIGNMENT) GSMatrix4x4 GSVector4 row(size_t i) const; GSVector4 col(size_t i) const; + GSMatrix4x4 Invert() const; + void store(void* m); float E[4][4]; From 22202f1607260d30e8a9b203d8917dd64ab7e3a0 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 22:02:34 +1000 Subject: [PATCH 08/22] Common: Fix vector blend32() and dot() on SSE2 --- src/common/gsvector_sse.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/common/gsvector_sse.h b/src/common/gsvector_sse.h index bfeb5567cd..a3fd46f836 100644 --- a/src/common/gsvector_sse.h +++ b/src/common/gsvector_sse.h @@ -2019,13 +2019,13 @@ class alignas(16) GSVector4 ALWAYS_INLINE GSVector4 hsub(const GSVector4& v) const { return GSVector4(_mm_hsub_ps(m, v.m)); } - ALWAYS_INLINE float dot(const GSVector4& v) const + NEVER_INLINE float dot(const GSVector4& v) const { #ifdef CPU_ARCH_SSE41 return _mm_cvtss_f32(_mm_dp_ps(m, v.m, 0xf1)); #else __m128 tmp = _mm_mul_ps(m, v.m); - tmp = _mm_add_ps(tmp, _mm_unpackhi_ps(tmp, tmp)); // (x+z, y+w, ..., ...) + tmp = _mm_add_ps(tmp, _mm_movehl_ps(tmp, tmp)); // (x+z, y+w, ..., ...) tmp = _mm_add_ss(tmp, _mm_shuffle_ps(tmp, tmp, _MM_SHUFFLE(3, 2, 1, 1))); return _mm_cvtss_f32(tmp); #endif @@ -2057,7 +2057,12 @@ class alignas(16) GSVector4 ALWAYS_INLINE GSVector4 blend32(const GSVector4& v, const GSVector4& mask) const { +#ifdef CPU_ARCH_SSE41 return GSVector4(_mm_blendv_ps(m, v, mask)); +#else + // NOTE: Assumes the entire lane is set with 1s or 0s. + return (v & mask) | andnot(mask); +#endif } ALWAYS_INLINE GSVector4 upl(const GSVector4& v) const { return GSVector4(_mm_unpacklo_ps(m, v)); } From dcd439e7d8315b06d1990be37ae01dbbb27a8637 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 21:28:34 +1000 Subject: [PATCH 09/22] GTE: Add 'Free Camera' feature --- src/core/gte.cpp | 314 +++++++++++++++++++++++++++++- src/core/gte.h | 9 + src/core/hotkeys.cpp | 96 +++++++++ src/core/imgui_overlays.cpp | 44 +++-- src/core/system.cpp | 5 +- src/duckstation-qt/mainwindow.cpp | 1 + src/duckstation-qt/mainwindow.ui | 9 + 7 files changed, 461 insertions(+), 17 deletions(-) diff --git a/src/core/gte.cpp b/src/core/gte.cpp index 6b0dcb1ffb..9ff322dff3 100644 --- a/src/core/gte.cpp +++ b/src/core/gte.cpp @@ -5,17 +5,26 @@ #include "cpu_core.h" #include "cpu_core_private.h" #include "cpu_pgxp.h" +#include "host.h" #include "settings.h" #include "util/state_wrapper.h" #include "common/assert.h" #include "common/bitutils.h" +#include "common/gsvector.h" +#include "common/timer.h" + +#include "imgui.h" #include #include +#include +#include #include +LOG_CHANNEL(Host); + namespace GTE { static constexpr s64 MAC0_MIN_VALUE = -(INT64_C(1) << 31); @@ -27,14 +36,42 @@ static constexpr s32 IR0_MAX_VALUE = 0x1000; static constexpr s32 IR123_MIN_VALUE = -(INT64_C(1) << 15); static constexpr s32 IR123_MAX_VALUE = (INT64_C(1) << 15) - 1; +static constexpr float FREECAM_MIN_TRANSLATION = -40000.0f; +static constexpr float FREECAM_MAX_TRANSLATION = 40000.0f; +static constexpr float FREECAM_MIN_ROTATION = -360.0f; +static constexpr float FREECAM_MAX_ROTATION = 360.0f; +static constexpr float FREECAM_DEFAULT_MOVE_SPEED = 4096.0f; +static constexpr float FREECAM_MAX_MOVE_SPEED = 65536.0f; +static constexpr float FREECAM_DEFAULT_TURN_SPEED = 30.0f; +static constexpr float FREECAM_MAX_TURN_SPEED = 360.0f; + namespace { + struct Config { DisplayAspectRatio aspect_ratio = DisplayAspectRatio::R4_3; u32 custom_aspect_ratio_numerator; u32 custom_aspect_ratio_denominator; float custom_aspect_ratio_f; + + ////////////////////////////////////////////////////////////////////////// + + Timer::Value freecam_update_time = 0; + std::atomic_bool freecam_transform_changed{false}; + bool freecam_enabled = false; + bool freecam_active = false; + + float freecam_move_speed = FREECAM_DEFAULT_MOVE_SPEED; + float freecam_turn_speed = FREECAM_DEFAULT_TURN_SPEED; + GSVector4 freecam_move = GSVector4::cxpr(0.0f); + GSVector4 freecam_turn = GSVector4::cxpr(0.0f); + + GSVector4 freecam_rotation = GSVector4::cxpr(0.0f); + GSVector4 freecam_translation = GSVector4::cxpr(0.0f); + + ALIGN_TO_CACHE_LINE GSMatrix4x4 freecam_matrix = GSMatrix4x4::Identity(); }; + } // namespace ALIGN_TO_CACHE_LINE static Config s_config; @@ -172,6 +209,7 @@ static void PushSZ(s32 value); static void PushRGBFromMAC(); static u32 UNRDivide(u32 lhs, u32 rhs); +static void ApplyFreecam(s64& x, s64& y, s64& z); static void MulMatVec(const s16* M_, const s16 Vx, const s16 Vy, const s16 Vz, u8 shift, bool lm); static void MulMatVec(const s16* M_, const s32 T[3], const s16 Vx, const s16 Vy, const s16 Vz, u8 shift, bool lm); static void MulMatVecBuggy(const s16* M_, const s32 T[3], const s16 Vx, const s16 Vy, const s16 Vz, u8 shift, bool lm); @@ -218,6 +256,8 @@ void GTE::Initialize() void GTE::Reset() { std::memset(®S, 0, sizeof(REGS)); + SetFreecamEnabled(false); + ResetFreecam(); } bool GTE::DoState(StateWrapper& sw) @@ -644,9 +684,11 @@ void GTE::RTPS(const s16 V[3], u8 shift, bool lm, bool last) // IR1 = MAC1 = (TRX*1000h + RT11*VX0 + RT12*VY0 + RT13*VZ0) SAR (sf*12) // IR2 = MAC2 = (TRY*1000h + RT21*VX0 + RT22*VY0 + RT23*VZ0) SAR (sf*12) // IR3 = MAC3 = (TRZ*1000h + RT31*VX0 + RT32*VY0 + RT33*VZ0) SAR (sf*12) - const s64 x = dot3(0); - const s64 y = dot3(1); - const s64 z = dot3(2); + s64 x = dot3(0); + s64 y = dot3(1); + s64 z = dot3(2); + if (s_config.freecam_active) + ApplyFreecam(x, y, z); TruncateAndSetMAC<1>(x, shift); TruncateAndSetMAC<2>(y, shift); TruncateAndSetMAC<3>(z, shift); @@ -1373,3 +1415,269 @@ GTE::InstructionImpl GTE::GetInstructionImpl(u32 inst_bits, TickCount* ticks) Panic("Missing handler"); } } + +bool GTE::IsFreecamEnabled() +{ + return s_config.freecam_enabled; +} + +void GTE::SetFreecamEnabled(bool enabled) +{ + if (s_config.freecam_enabled == enabled) + return; + + s_config.freecam_enabled = enabled; + if (enabled) + { + s_config.freecam_transform_changed.store(true, std::memory_order_release); + s_config.freecam_update_time = Timer::GetCurrentValue(); + } +} + +void GTE::SetFreecamMoveAxis(u32 axis, float x) +{ + DebugAssert(axis < 3); + s_config.freecam_move.F32[axis] = x; + SetFreecamEnabled(true); +} + +void GTE::SetFreecamRotateAxis(u32 axis, float x) +{ + DebugAssert(axis < 3); + s_config.freecam_turn.F32[axis] = x; + SetFreecamEnabled(true); +} + +void GTE::UpdateFreecam(u64 current_time) +{ + if (!s_config.freecam_enabled) + { + s_config.freecam_active = false; + return; + } + + const float dt = std::clamp( + static_cast(Timer::ConvertValueToSeconds(current_time - s_config.freecam_update_time)), 0.0f, 1.0f); + s_config.freecam_update_time = current_time; + + bool changed = true; + s_config.freecam_transform_changed.compare_exchange_strong(changed, false, std::memory_order_acq_rel); + + if (!(s_config.freecam_move == GSVector4::zero()).alltrue()) + { + s_config.freecam_translation += s_config.freecam_move * GSVector4(s_config.freecam_move_speed * dt); + changed = true; + } + + if (!(s_config.freecam_turn == GSVector4::zero()).alltrue()) + { + s_config.freecam_rotation += s_config.freecam_turn * GSVector4(s_config.freecam_turn_speed * + static_cast(std::numbers::pi / 180.0) * dt); + + // wrap around -360 degrees/360 degrees + constexpr GSVector4 min_rot = GSVector4::cxpr(static_cast(std::numbers::pi * -2.0)); + constexpr GSVector4 max_rot = GSVector4::cxpr(static_cast(std::numbers::pi * 2.0)); + s_config.freecam_rotation = + s_config.freecam_rotation.blend32(s_config.freecam_rotation + max_rot, (s_config.freecam_rotation < min_rot)); + s_config.freecam_rotation = + s_config.freecam_rotation.blend32(s_config.freecam_rotation + min_rot, (s_config.freecam_rotation > max_rot)); + + changed = true; + } + + if (!changed) + return; + + bool any_xform = false; + s_config.freecam_matrix = GSMatrix4x4::Identity(); + + // translate than rotate, since the camera is rotating around a point + // remember, matrix transformation happens in the opposite of the multiplication order + + if (s_config.freecam_translation.x != 0.0f || s_config.freecam_translation.y != 0.0f || + s_config.freecam_translation.z != 0.0f) + { + s_config.freecam_matrix = GSMatrix4x4::Translation(s_config.freecam_translation.x, s_config.freecam_translation.y, + s_config.freecam_translation.z); + any_xform = true; + } + + if (s_config.freecam_rotation.z != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::RotationZ(s_config.freecam_rotation.z); + any_xform = true; + } + + if (s_config.freecam_rotation.y != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::RotationY(s_config.freecam_rotation.y); + any_xform = true; + } + + if (s_config.freecam_rotation.x != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::RotationX(s_config.freecam_rotation.x); + any_xform = true; + } + + s_config.freecam_active = any_xform; +} + +void GTE::ResetFreecam() +{ + s_config.freecam_active = false; + s_config.freecam_rotation = GSVector4::zero(); + s_config.freecam_translation = GSVector4::zero(); + s_config.freecam_transform_changed.store(false, std::memory_order_release); +} + +void GTE::ApplyFreecam(s64& x, s64& y, s64& z) +{ + constexpr double scale = 1 << 12; + + GSVector4 xyz(static_cast(static_cast(x) / scale), static_cast(static_cast(y) / scale), + static_cast(static_cast(z) / scale), 1.0f); + + xyz = s_config.freecam_matrix * xyz; + + x = static_cast(static_cast(xyz.x) * scale); + y = static_cast(static_cast(xyz.y) * scale); + z = static_cast(static_cast(xyz.z) * scale); +} + +void GTE::DrawFreecamWindow(float scale) +{ + const ImGuiStyle& style = ImGui::GetStyle(); + + bool freecam_enabled = s_config.freecam_enabled; + bool enabled_changed = false; + + const float label_width = 140.0f * scale; + const float item_width = 350.0f * scale; + const float padding_height = 5.0f * scale; + + if (ImGui::CollapsingHeader("Settings", ImGuiTreeNodeFlags_DefaultOpen)) + { + const float third_width = 50.0f * scale; + const float second_width = item_width - third_width; + + enabled_changed = ImGui::Checkbox("Enable Freecam", &freecam_enabled); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padding_height); + + ImGui::Columns(3, "Settings", false); + ImGui::SetColumnWidth(0, label_width); + ImGui::SetColumnWidth(1, second_width); + ImGui::SetColumnWidth(2, third_width); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.ItemInnerSpacing.y); + ImGui::TextUnformatted("Movement Speed:"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(second_width); + ImGui::DragFloat("##MovementSpeed", &s_config.freecam_move_speed, 1.0f, 0.0f, FREECAM_MAX_MOVE_SPEED); + ImGui::NextColumn(); + if (ImGui::Button("Reset##ResetMovementSpeed")) + s_config.freecam_move_speed = FREECAM_DEFAULT_MOVE_SPEED; + ImGui::NextColumn(); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.ItemInnerSpacing.y); + ImGui::TextUnformatted("Turning Speed:"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(second_width); + ImGui::DragFloat("##TurnSpeed", &s_config.freecam_turn_speed, 1.0f, 0.0f, FREECAM_MAX_TURN_SPEED); + ImGui::NextColumn(); + if (ImGui::Button("Reset##ResetTurnSpeed")) + s_config.freecam_turn_speed = FREECAM_DEFAULT_TURN_SPEED; + ImGui::NextColumn(); + + ImGui::Columns(1); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padding_height); + } + + bool changed = false; + + if (ImGui::CollapsingHeader("Rotation", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Columns(2, "Rotation", false); + ImGui::SetColumnWidth(0, label_width); + ImGui::SetColumnWidth(1, item_width); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.ItemInnerSpacing.y); + ImGui::TextUnformatted("X Rotation (Pitch):"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(item_width); + changed |= ImGui::SliderAngle("##XRot", &s_config.freecam_rotation.x, FREECAM_MIN_ROTATION, FREECAM_MAX_ROTATION); + ImGui::NextColumn(); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.ItemInnerSpacing.y); + ImGui::TextUnformatted("Y Rotation (Yaw):"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(item_width); + changed |= ImGui::SliderAngle("##YRot", &s_config.freecam_rotation.y, FREECAM_MIN_ROTATION, FREECAM_MAX_ROTATION); + ImGui::NextColumn(); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.ItemInnerSpacing.y); + ImGui::TextUnformatted("Z Rotation (Roll):"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(item_width); + changed |= ImGui::SliderAngle("##ZRot", &s_config.freecam_rotation.z, FREECAM_MIN_ROTATION, FREECAM_MAX_ROTATION); + ImGui::NextColumn(); + + ImGui::Columns(1); + + if (ImGui::Button("Reset##ResetRotation")) + { + s_config.freecam_rotation = GSVector4::zero(); + changed = true; + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padding_height); + } + + if (ImGui::CollapsingHeader("Translation", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Columns(2, "Translation", false); + ImGui::SetColumnWidth(0, label_width); + ImGui::SetColumnWidth(1, item_width); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.ItemInnerSpacing.y); + ImGui::TextUnformatted("X Offset:"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(item_width); + changed |= ImGui::DragFloat("##XOffset", &s_config.freecam_translation.x, 1.0f, FREECAM_MIN_TRANSLATION, + FREECAM_MAX_TRANSLATION, "%.1f", ImGuiSliderFlags_None); + ImGui::NextColumn(); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.ItemInnerSpacing.y); + ImGui::TextUnformatted("Y Offset:"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(item_width); + changed |= ImGui::DragFloat("##YOffset", &s_config.freecam_translation.y, 1.0f, FREECAM_MIN_TRANSLATION, + FREECAM_MAX_TRANSLATION, "%.1f", ImGuiSliderFlags_None); + ImGui::NextColumn(); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.ItemInnerSpacing.y); + ImGui::TextUnformatted("Z Offset:"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(item_width); + changed |= ImGui::DragFloat("##ZOffset", &s_config.freecam_translation.z, 1.0f, FREECAM_MIN_TRANSLATION, + FREECAM_MAX_TRANSLATION, "%.1f", ImGuiSliderFlags_None); + ImGui::NextColumn(); + + ImGui::Columns(1); + + if (ImGui::Button("Reset##ResetTranslation")) + { + s_config.freecam_translation = GSVector4::zero(); + changed = true; + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padding_height); + } + + if (enabled_changed || (!freecam_enabled && changed)) + Host::RunOnCPUThread([enabled = freecam_enabled || changed]() { SetFreecamEnabled(enabled); }); + + if (changed) + s_config.freecam_transform_changed.store(true, std::memory_order_release); +} diff --git a/src/core/gte.h b/src/core/gte.h index eeb7022517..6679005c89 100644 --- a/src/core/gte.h +++ b/src/core/gte.h @@ -27,4 +27,13 @@ void ExecuteInstruction(u32 inst_bits); using InstructionImpl = void (*)(Instruction); InstructionImpl GetInstructionImpl(u32 inst_bits, TickCount* ticks); +void DrawFreecamWindow(float scale); + +bool IsFreecamEnabled(); +void SetFreecamEnabled(bool enabled); +void SetFreecamMoveAxis(u32 axis, float x); +void SetFreecamRotateAxis(u32 axis, float x); +void UpdateFreecam(u64 current_time); +void ResetFreecam(); + } // namespace GTE diff --git a/src/core/hotkeys.cpp b/src/core/hotkeys.cpp index 3f4377e59c..f0883d08fd 100644 --- a/src/core/hotkeys.cpp +++ b/src/core/hotkeys.cpp @@ -9,6 +9,7 @@ #include "gpu.h" #include "gpu_hw_texture_cache.h" #include "gpu_thread.h" +#include "gte.h" #include "host.h" #include "imgui_overlays.h" #include "settings.h" @@ -520,6 +521,101 @@ DEFINE_HOTKEY("RotateCounterclockwise", TRANSLATE_NOOP("Hotkeys", "Graphics"), } }) +DEFINE_HOTKEY("FreecamToggle", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Freecam Toggle"), + [](s32 pressed) { + if (!pressed && !Achievements::IsHardcoreModeActive()) + GTE::SetFreecamEnabled(!GTE::IsFreecamEnabled()); + }) +DEFINE_HOTKEY("FreecamReset", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Freecam Reset"), + [](s32 pressed) { + if (!pressed && !Achievements::IsHardcoreModeActive()) + GTE::ResetFreecam(); + }) +DEFINE_HOTKEY("FreecamMoveLeft", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Freecam Move Left"), + [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamMoveAxis(0, std::max(static_cast(pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamMoveRight", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Freecam Move Right"), [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamMoveAxis(0, std::min(static_cast(-pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamMoveUp", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Freecam Move Up"), + [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamMoveAxis(1, std::max(static_cast(pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamMoveDown", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Freecam Move Down"), + [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamMoveAxis(1, std::min(static_cast(-pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamMoveForward", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Freecam Move Forward"), [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamMoveAxis(2, std::min(static_cast(-pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamMoveBackward", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Freecam Move Backward"), [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamMoveAxis(2, std::max(static_cast(pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamRotateLeft", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Freecam Rotate Left"), [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamRotateAxis(1, std::max(static_cast(pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamRotateRight", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Freecam Rotate Right"), [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamRotateAxis(1, std::min(static_cast(-pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamRotateForward", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Freecam Rotate Forward"), [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamRotateAxis(0, std::min(static_cast(-pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamRotateBackward", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Freecam Rotate Backward"), [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamRotateAxis(0, std::max(static_cast(pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamRollLeft", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Freecam Roll Left"), + [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamRotateAxis(2, std::min(static_cast(-pressed), 0.0f)); + }) +DEFINE_HOTKEY("FreecamRollRight", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Freecam Roll Right"), [](s32 pressed) { + if (Achievements::IsHardcoreModeActive()) + return; + + GTE::SetFreecamRotateAxis(2, std::max(static_cast(pressed), 0.0f)); + }) + DEFINE_HOTKEY("AudioMute", TRANSLATE_NOOP("Hotkeys", "Audio"), TRANSLATE_NOOP("Hotkeys", "Toggle Mute"), [](s32 pressed) { if (!pressed && System::IsValid()) diff --git a/src/core/imgui_overlays.cpp b/src/core/imgui_overlays.cpp index 160a534ca7..7b3677be47 100644 --- a/src/core/imgui_overlays.cpp +++ b/src/core/imgui_overlays.cpp @@ -11,6 +11,7 @@ #include "gpu.h" #include "gpu_backend.h" #include "gpu_thread.h" +#include "gte.h" #include "host.h" #include "mdec.h" #include "performance_counters.h" @@ -72,6 +73,7 @@ struct DebugWindowInfo } // namespace static void FormatProcessorStat(SmallStringBase& text, double usage, double time); +static void SetStatusIndicatorIcons(SmallStringBase& text, bool paused); static void DrawPerformanceOverlay(const GPUBackend* gpu, float& position_y, float scale, float margin, float spacing); static void DrawMediaCaptureOverlay(float& position_y, float scale, float margin, float spacing); static void DrawFrameTimeOverlay(float& position_y, float scale, float margin, float spacing); @@ -80,9 +82,10 @@ static void DrawInputsOverlay(); #ifndef __ANDROID__ -static constexpr size_t NUM_DEBUG_WINDOWS = 6; +static constexpr size_t NUM_DEBUG_WINDOWS = 7; static constexpr const char* DEBUG_WINDOW_CONFIG_SECTION = "DebugWindows"; static constexpr const std::array s_debug_window_info = {{ + {"Freecam", "Free Camera", ":icons/applications-system.png", >E::DrawFreecamWindow, 500, 400}, {"SPU", "SPU State", ":icons/applications-system.png", &SPU::DrawDebugStateWindow, 800, 915}, {"CDROM", "CD-ROM State", ":icons/applications-system.png", &CDROM::DrawDebugWindow, 800, 540}, {"GPU", "GPU State", ":icons/applications-system.png", [](float sc) { g_gpu.DrawDebugStateWindow(sc); }, 450, 550}, @@ -250,6 +253,27 @@ void ImGuiManager::FormatProcessorStat(SmallStringBase& text, double usage, doub text.append_format("{:.1f}% ({:.2f}ms)", usage, time); } +void ImGuiManager::SetStatusIndicatorIcons(SmallStringBase& text, bool paused) +{ + text.clear(); + if (GTE::IsFreecamEnabled()) + text.append(ICON_EMOJI_MAGNIFIYING_GLASS_TILTED_LEFT " "); + + if (paused) + { + text.append(ICON_EMOJI_PAUSE); + } + else + { + const bool rewinding = System::IsRewinding(); + if (rewinding || System::IsFastForwardEnabled() || System::IsTurboEnabled()) + text.append(rewinding ? ICON_EMOJI_FAST_REVERSE : ICON_EMOJI_FAST_FORWARD); + } + + if (!text.empty() && text.back() == ' ') + text.pop_back(); +} + void ImGuiManager::DrawPerformanceOverlay(const GPUBackend* gpu, float& position_y, float scale, float margin, float spacing) { @@ -282,8 +306,7 @@ void ImGuiManager::DrawPerformanceOverlay(const GPUBackend* gpu, float& position position_y += text_size.y + spacing; \ } while (0) - const System::State state = System::GetState(); - if (state == System::State::Running) + if (!GPUThread::IsSystemPaused()) { const float speed = PerformanceCounters::GetEmulationSpeed(); if (g_gpu_settings.display_show_fps) @@ -415,18 +438,14 @@ void ImGuiManager::DrawPerformanceOverlay(const GPUBackend* gpu, float& position if (g_gpu_settings.display_show_status_indicators) { - const bool rewinding = System::IsRewinding(); - if (rewinding || System::IsFastForwardEnabled() || System::IsTurboEnabled()) - { - text.assign(rewinding ? ICON_EMOJI_FAST_REVERSE : ICON_EMOJI_FAST_FORWARD); + SetStatusIndicatorIcons(text, false); + if (!text.empty()) DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255)); - } } } - else if (g_gpu_settings.display_show_status_indicators && state == System::State::Paused && - !FullscreenUI::HasActiveWindow()) + else if (g_gpu_settings.display_show_status_indicators && !FullscreenUI::HasActiveWindow()) { - text.assign(ICON_EMOJI_PAUSE); + SetStatusIndicatorIcons(text, true); DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255)); } @@ -457,7 +476,8 @@ void ImGuiManager::DrawEnhancementsOverlay(const GPUBackend* gpu) text.append_format(" IR={}x", g_gpu_settings.gpu_resolution_scale); if (g_gpu_settings.gpu_multisamples != 1) { - text.append_format(" {}x{}", g_gpu_settings.gpu_multisamples, g_gpu_settings.gpu_per_sample_shading ? "SSAA" : "MSAA"); + text.append_format(" {}x{}", g_gpu_settings.gpu_multisamples, + g_gpu_settings.gpu_per_sample_shading ? "SSAA" : "MSAA"); } if (g_gpu_settings.gpu_true_color) text.append(" TrueCol"); diff --git a/src/core/system.cpp b/src/core/system.cpp index 270f6e89d2..5a3fd03553 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -2104,6 +2104,9 @@ void System::FrameDone() SaveMemoryState(AllocateMemoryState()); } + Timer::Value current_time = Timer::GetCurrentValue(); + GTE::UpdateFreecam(current_time); + // Frame step after runahead, otherwise the pause takes precedence and the replay never happens. if (s_state.frame_step_request) { @@ -2111,8 +2114,6 @@ void System::FrameDone() PauseSystem(true); } - Timer::Value current_time = Timer::GetCurrentValue(); - // pre-frame sleep accounting (input lag reduction) const Timer::Value pre_frame_sleep_until = s_state.next_frame_time + s_state.pre_frame_sleep_time; s_state.last_active_frame_time = current_time - s_state.frame_start_time; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index d42cfc9e54..bb46b26c9e 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -2134,6 +2134,7 @@ void MainWindow::connectSignals() g_emu_thread->dumpSPURAM(filename); }); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowVRAM, "Debug", "ShowVRAM", false); + SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionFreeCamera, "DebugWindows", "Freecam", false); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowGPUState, "DebugWindows", "GPU", false); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowCDROMState, "DebugWindows", "CDROM", false); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowSPUState, "DebugWindows", "SPU", false); diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 6455e859ca..42b5997ed1 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -231,6 +231,7 @@ + @@ -970,6 +971,14 @@ ISO Browser + + + true + + + Free Camera + + From 1d63648d688af5bf6461db3dbb4c9da0b9146f97 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 21:37:45 +1000 Subject: [PATCH 10/22] Qt: Forward text input to aux render windows Fixes text input in freecam window. --- src/duckstation-qt/displaywidget.cpp | 24 ++++++++++++++++++++++-- src/util/imgui_manager.cpp | 6 ++++++ src/util/imgui_manager.h | 1 + 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/duckstation-qt/displaywidget.cpp b/src/duckstation-qt/displaywidget.cpp index 15beea1a5e..5ecf31ed2a 100644 --- a/src/duckstation-qt/displaywidget.cpp +++ b/src/duckstation-qt/displaywidget.cpp @@ -497,12 +497,32 @@ bool AuxiliaryDisplayWidget::event(QEvent* event) case QEvent::KeyPress: case QEvent::KeyRelease: { + const QKeyEvent* key_event = static_cast(event); + + if (type == QEvent::KeyPress) + { + // note this won't work for emojis.. deal with that if it's ever needed + for (const QChar& ch : key_event->text()) + { + // Don't forward backspace characters. We send the backspace as a normal key event, + // so if we send the character too, it double-deletes. + if (ch == QChar('\b')) + break; + + g_emu_thread->queueAuxiliaryRenderWindowInputEvent( + m_userdata, Host::AuxiliaryRenderWindowEvent::TextEntered, + Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast(ch.unicode())}); + } + } + + if (key_event->isAutoRepeat()) + return true; + g_emu_thread->queueAuxiliaryRenderWindowInputEvent( m_userdata, (type == QEvent::KeyPress) ? Host::AuxiliaryRenderWindowEvent::KeyPressed : Host::AuxiliaryRenderWindowEvent::KeyReleased, - Host::AuxiliaryRenderWindowEventParam{.uint_param = - static_cast(static_cast(event)->key())}); + Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast(key_event->key())}); return true; } diff --git a/src/util/imgui_manager.cpp b/src/util/imgui_manager.cpp index 2d8b196d8a..0ab8406851 100644 --- a/src/util/imgui_manager.cpp +++ b/src/util/imgui_manager.cpp @@ -1534,6 +1534,12 @@ void ImGuiManager::ProcessAuxiliaryRenderWindowInputEvent(Host::AuxiliaryRenderW } break; + case Host::AuxiliaryRenderWindowEvent::TextEntered: + { + io.AddInputCharacter(param1.uint_param); + } + break; + case Host::AuxiliaryRenderWindowEvent::MouseMoved: { io.MousePos.x = param1.float_param; diff --git a/src/util/imgui_manager.h b/src/util/imgui_manager.h index 785a5617f3..5b61b51169 100644 --- a/src/util/imgui_manager.h +++ b/src/util/imgui_manager.h @@ -33,6 +33,7 @@ enum class AuxiliaryRenderWindowEvent : u8 Resized, KeyPressed, KeyReleased, + TextEntered, MouseMoved, MousePressed, MouseReleased, From 5ac5a1d2468e7b64df0072a9abd1378074e4edf2 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 21:42:17 +1000 Subject: [PATCH 11/22] Hotkeys: Fix resolution scale with memory save states --- src/core/hotkeys.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/hotkeys.cpp b/src/core/hotkeys.cpp index f0883d08fd..c09c481bc5 100644 --- a/src/core/hotkeys.cpp +++ b/src/core/hotkeys.cpp @@ -60,8 +60,8 @@ static void HotkeyModifyResolutionScale(s32 increment) if (System::IsValid()) { - System::ClearMemorySaveStates(true, false); GPUThread::UpdateSettings(true, false); + System::ClearMemorySaveStates(true, false); } } From f3b7686457f7bcef394344d3667b88553ceb1ab6 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 22:55:29 +1000 Subject: [PATCH 12/22] System: Fix crash with memory save states + renderer switch --- src/core/system.cpp | 82 +++++++++++++++++++++------------------ src/core/system_private.h | 2 +- 2 files changed, 45 insertions(+), 39 deletions(-) diff --git a/src/core/system.cpp b/src/core/system.cpp index 5a3fd03553..768f2e0711 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1167,7 +1167,7 @@ DiscRegion System::GetRegionForPsf(const char* path) void System::RecreateGPU(GPURenderer renderer) { - FreeMemoryStateStorage(false); + FreeMemoryStateStorage(false, true, false); StopMediaCapture(); Error error; @@ -1954,7 +1954,7 @@ void System::DestroySystem() if (g_settings.inhibit_screensaver) PlatformMisc::ResumeScreensaver(); - FreeMemoryStateStorage(false); + FreeMemoryStateStorage(true, true, false); Cheats::UnloadAll(); PCDrv::Shutdown(); @@ -2502,7 +2502,7 @@ bool System::AllocateMemoryStates(size_t state_count, bool recycle_old_textures) if (state_count != s_state.memory_save_states.size()) { - FreeMemoryStateStorage(recycle_old_textures); + FreeMemoryStateStorage(true, true, recycle_old_textures); s_state.memory_save_states.resize(state_count); } @@ -2523,7 +2523,7 @@ bool System::AllocateMemoryStates(size_t state_count, bool recycle_old_textures) ERROR_LOG("Failed to allocate {} memory save states: {}", s_state.memory_save_states.size(), error.GetDescription()); ERROR_LOG("Disabling runahead/rewind."); - FreeMemoryStateStorage(false); + FreeMemoryStateStorage(true, true, false); s_state.runahead_frames = 0; s_state.memory_save_state_front = 0; s_state.memory_save_state_count = 0; @@ -2546,50 +2546,56 @@ void System::ClearMemorySaveStates(bool reallocate_resources, bool recycle_textu AllocateMemoryStates(s_state.memory_save_states.size(), recycle_textures); } -void System::FreeMemoryStateStorage(bool recycle_textures) +void System::FreeMemoryStateStorage(bool release_memory, bool release_textures, bool recycle_textures) { - // TODO: use non-copyable function, that way we don't need to store raw pointers - std::vector textures; - bool gpu_thread_synced = false; - - for (MemorySaveState& mss : s_state.memory_save_states) + if (release_memory || release_textures) { - if ((mss.vram_texture || !mss.gpu_state_data.empty()) && !gpu_thread_synced) - { - gpu_thread_synced = true; - GPUThread::SyncGPUThread(true); - } + // TODO: use non-copyable function, that way we don't need to store raw pointers + std::vector textures; + bool gpu_thread_synced = false; - if (mss.vram_texture) + for (MemorySaveState& mss : s_state.memory_save_states) { - if (textures.empty()) - textures.reserve(s_state.memory_save_states.size()); + if ((mss.vram_texture || !mss.gpu_state_data.empty()) && !gpu_thread_synced) + { + gpu_thread_synced = true; + GPUThread::SyncGPUThread(true); + } - textures.push_back(mss.vram_texture.release()); + if (mss.vram_texture) + { + if (textures.empty()) + textures.reserve(s_state.memory_save_states.size()); + + textures.push_back(mss.vram_texture.release()); + } + + mss.gpu_state_data.deallocate(); + mss.gpu_state_size = 0; + mss.state_data.deallocate(); + mss.state_size = 0; } - mss.gpu_state_data.deallocate(); - mss.gpu_state_size = 0; - mss.state_data.deallocate(); - mss.state_size = 0; + if (!textures.empty()) + { + GPUThread::RunOnThread([textures = std::move(textures), recycle_textures]() mutable { + for (GPUTexture* texture : textures) + { + if (recycle_textures) + g_gpu_device->RecycleTexture(std::unique_ptr(texture)); + else + delete texture; + } + }); + } } - if (!textures.empty()) + if (release_memory) { - GPUThread::RunOnThread([textures = std::move(textures), recycle_textures]() mutable { - for (GPUTexture* texture : textures) - { - if (recycle_textures) - g_gpu_device->RecycleTexture(std::unique_ptr(texture)); - else - delete texture; - } - }); + s_state.memory_save_states = std::vector(); + s_state.memory_save_state_front = 0; + s_state.memory_save_state_count = 0; } - - s_state.memory_save_states = std::vector(); - s_state.memory_save_state_front = 0; - s_state.memory_save_state_count = 0; } void System::LoadMemoryState(MemorySaveState& mss, bool update_display) @@ -4860,7 +4866,7 @@ void System::CalculateRewindMemoryUsage(u32 num_saves, u32 resolution_scale, u64 void System::UpdateMemorySaveStateSettings() { const bool any_memory_states_active = (g_settings.IsRunaheadEnabled() || g_settings.rewind_enable); - FreeMemoryStateStorage(any_memory_states_active); + FreeMemoryStateStorage(true, true, any_memory_states_active); if (IsReplayingGPUDump()) [[unlikely]] { diff --git a/src/core/system_private.h b/src/core/system_private.h index 74b874a228..e47d3d0b55 100644 --- a/src/core/system_private.h +++ b/src/core/system_private.h @@ -27,7 +27,7 @@ MemorySaveState& AllocateMemoryState(); MemorySaveState& GetFirstMemoryState(); MemorySaveState& PopMemoryState(); bool AllocateMemoryStates(size_t state_count, bool recycle_old_textures); -void FreeMemoryStateStorage(bool recycle_texture); +void FreeMemoryStateStorage(bool release_memory, bool release_textures, bool recycle_textures); void LoadMemoryState(MemorySaveState& mss, bool update_display); void SaveMemoryState(MemorySaveState& mss); From 37e5e64ddc0bd129d8b272396be5c8657f7e3a1d Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 1 Jan 2025 23:05:07 +1000 Subject: [PATCH 13/22] System: Move state display updates to call sites Fixes black frames when changing settings with runahead/rewind enabled. --- src/core/gpu.cpp | 14 ++------------ src/core/gpu.h | 8 +++++--- src/core/system.cpp | 20 +++++++++++++++++--- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp index 356bc099af..86a295fdae 100644 --- a/src/core/gpu.cpp +++ b/src/core/gpu.cpp @@ -243,7 +243,7 @@ void GPU::SoftReset() UpdateGPUIdle(); } -bool GPU::DoState(StateWrapper& sw, bool update_display) +bool GPU::DoState(StateWrapper& sw) { if (sw.IsWriting()) { @@ -378,10 +378,6 @@ bool GPU::DoState(StateWrapper& sw, bool update_display) UpdateDMARequest(); UpdateCRTCConfig(); UpdateCommandTickEvent(); - - // If we're paused, need to update the display FB. - if (update_display) - UpdateDisplay(false); } else // if not memory state { @@ -395,7 +391,7 @@ bool GPU::DoState(StateWrapper& sw, bool update_display) return !sw.HasError(); } -void GPU::DoMemoryState(StateWrapper& sw, System::MemorySaveState& mss, bool update_display) +void GPU::DoMemoryState(StateWrapper& sw, System::MemorySaveState& mss) { sw.Do(&m_GPUSTAT.bits); @@ -438,12 +434,6 @@ void GPU::DoMemoryState(StateWrapper& sw, System::MemorySaveState& mss, bool upd sizeof(GPUBackendDoMemoryStateCommand))); cmd->memory_save_state = &mss; GPUThread::PushCommandAndWakeThread(cmd); - - if (update_display) - { - DebugAssert(sw.IsReading()); - UpdateDisplay(false); - } } void GPU::UpdateDMARequest() diff --git a/src/core/gpu.h b/src/core/gpu.h index 595d9012a0..e7a327865e 100644 --- a/src/core/gpu.h +++ b/src/core/gpu.h @@ -97,8 +97,8 @@ class GPU final void Initialize(); void Shutdown(); void Reset(bool clear_vram); - bool DoState(StateWrapper& sw, bool update_display); - void DoMemoryState(StateWrapper& sw, System::MemorySaveState& mss, bool update_display); + bool DoState(StateWrapper& sw); + void DoMemoryState(StateWrapper& sw, System::MemorySaveState& mss); // Render statistics debug window. void DrawDebugStateWindow(float scale); @@ -216,6 +216,9 @@ class GPU final // Dumps raw VRAM to a file. bool DumpVRAMToFile(const char* filename); + // Kicks the current frame to the backend for display. + void UpdateDisplay(bool submit_frame); + // Queues the current frame for presentation. Should only be used with runahead. void QueuePresentCurrentFrame(); @@ -296,7 +299,6 @@ class GPU final void ReadVRAM(u16 x, u16 y, u16 width, u16 height); void UpdateVRAM(u16 x, u16 y, u16 width, u16 height, const void* data, bool set_mask, bool check_mask); - void UpdateDisplay(bool submit_frame); void PrepareForDraw(); void FinishPolyline(); diff --git a/src/core/system.cpp b/src/core/system.cpp index 768f2e0711..8ea4f13255 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1178,6 +1178,10 @@ void System::RecreateGPU(GPURenderer renderer) } ClearMemorySaveStates(true, false); + + g_gpu.UpdateDisplay(false); + if (IsPaused()) + GPUThread::PresentCurrentFrame(); } void System::LoadSettings(bool display_osd_messages) @@ -2390,7 +2394,7 @@ bool System::DoState(StateWrapper& sw, bool update_display) if (!sw.DoMarker("InterruptController") || !InterruptController::DoState(sw)) return false; - if (!sw.DoMarker("GPU") || !g_gpu.DoState(sw, update_display)) + if (!sw.DoMarker("GPU") || !g_gpu.DoState(sw)) return false; if (!sw.DoMarker("CDROM") || !CDROM::DoState(sw)) @@ -2459,7 +2463,14 @@ bool System::DoState(StateWrapper& sw, bool update_display) Achievements::ResetClient(); } - return !sw.HasError(); + if (sw.HasError()) + return false; + + // If we're paused, need to update the display FB. + if (update_display) + g_gpu.UpdateDisplay(false); + + return true; } System::MemorySaveState& System::AllocateMemoryState() @@ -2665,7 +2676,7 @@ void System::DoMemoryState(StateWrapper& sw, MemorySaveState& mss, bool update_d SAVE_COMPONENT("DMA", DMA::DoState(sw)); SAVE_COMPONENT("InterruptController", InterruptController::DoState(sw)); - g_gpu.DoMemoryState(sw, mss, update_display); + g_gpu.DoMemoryState(sw, mss); SAVE_COMPONENT("CDROM", CDROM::DoState(sw)); SAVE_COMPONENT("Pad", Pad::DoState(sw, true)); @@ -2677,6 +2688,9 @@ void System::DoMemoryState(StateWrapper& sw, MemorySaveState& mss, bool update_d SAVE_COMPONENT("Achievements", Achievements::DoState(sw)); #undef SAVE_COMPONENT + + if (update_display) + g_gpu.UpdateDisplay(false); } bool System::LoadBIOS(Error* error) From c11468b9f18940c368ef629bef4ca8da9dab69cd Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 2 Jan 2025 00:14:10 +1000 Subject: [PATCH 14/22] Qt: Drop log messages if rate is too high Prevents the application locking up and memory usage going bananas if log messages do end up spammed at crazy rates. --- src/duckstation-qt/logwindow.cpp | 61 +++++++++++++++++++++++--------- src/duckstation-qt/logwindow.h | 13 +++++-- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/duckstation-qt/logwindow.cpp b/src/duckstation-qt/logwindow.cpp index 25e220a35f..61827fd62e 100644 --- a/src/duckstation-qt/logwindow.cpp +++ b/src/duckstation-qt/logwindow.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "logwindow.h" @@ -177,6 +177,7 @@ void LogWindow::createUi() m_text->setUndoRedoEnabled(false); m_text->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); m_text->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + m_text->setMaximumBlockCount(MAX_LINES); #if defined(_WIN32) QFont font("Consolas"); @@ -271,6 +272,8 @@ void LogWindow::logCallback(void* pUserParam, Log::MessageCategory cat, const ch const QLatin1StringView qchannel( (Log::UnpackLevel(cat) <= Log::Level::Warning) ? functionName : Log::GetChannelName(Log::UnpackChannel(cat))); + this_ptr->m_lines_pending.fetch_add(1, std::memory_order_acq_rel); + if (QThread::isMainThread()) { this_ptr->appendMessage(qchannel, static_cast(cat), qmessage); @@ -307,6 +310,43 @@ void LogWindow::changeEvent(QEvent* event) } void LogWindow::appendMessage(const QLatin1StringView& channel, quint32 cat, const QString& message) +{ + const int num_lines_still_pending = m_lines_pending.fetch_sub(1, std::memory_order_acq_rel) - 1; + if (m_lines_to_skip > 0) + { + m_lines_to_skip--; + return; + } + + if (num_lines_still_pending > MAX_LINES) + { + realAppendMessage( + QLatin1StringView(Log::GetChannelName(Log::Channel::Log)), + Log::PackCategory(Log::Channel::Log, Log::Level::Warning, Log::Color::StrongYellow), + tr("Dropped %1 log messages, please use file or system console logging.\n").arg(num_lines_still_pending)); + m_lines_to_skip = num_lines_still_pending; + return; + } + else if (num_lines_still_pending > BLOCK_UPDATES_THRESHOLD) + { + if (m_text->updatesEnabled()) + { + m_text->setUpdatesEnabled(false); + m_text->document()->blockSignals(true); + m_text->blockSignals(true); + } + } + else if (!m_text->updatesEnabled()) + { + m_text->blockSignals(false); + m_text->document()->blockSignals(false); + m_text->setUpdatesEnabled(true); + } + + realAppendMessage(channel, cat, message); +} + +void LogWindow::realAppendMessage(const QLatin1StringView& channel, quint32 cat, const QString& message) { QTextCursor temp_cursor = m_text->textCursor(); QScrollBar* scrollbar = m_text->verticalScrollBar(); @@ -370,6 +410,7 @@ void LogWindow::appendMessage(const QLatin1StringView& channel, quint32 cat, con QTextCharFormat format = temp_cursor.charFormat(); const size_t dark = static_cast(m_is_dark_theme); + temp_cursor.beginEditBlock(); if (Log::AreTimestampsEnabled()) { const float message_time = Log::GetCurrentMessageTime(); @@ -395,23 +436,11 @@ void LogWindow::appendMessage(const QLatin1StringView& channel, quint32 cat, con format.setForeground(QBrush(message_colors[dark][static_cast(color)])); temp_cursor.setCharFormat(format); temp_cursor.insertText(message); + temp_cursor.endEditBlock(); } - if (cursor_at_end) - { - if (scroll_at_end) - { - m_text->setTextCursor(temp_cursor); - scrollbar->setSliderPosition(scrollbar->maximum()); - } - else - { - // Can't let changing the cursor affect the scroll bar... - const int pos = scrollbar->sliderPosition(); - m_text->setTextCursor(temp_cursor); - scrollbar->setSliderPosition(pos); - } - } + if (cursor_at_end && scroll_at_end) + m_text->centerCursor(); } void LogWindow::saveSize() diff --git a/src/duckstation-qt/logwindow.h b/src/duckstation-qt/logwindow.h index 1c7f6a7a2d..1a05adfd7e 100644 --- a/src/duckstation-qt/logwindow.h +++ b/src/duckstation-qt/logwindow.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once @@ -7,9 +7,11 @@ #include #include + +#include #include -class LogWindow : public QMainWindow +class ALIGN_TO_CACHE_LINE LogWindow : public QMainWindow { Q_OBJECT @@ -43,10 +45,13 @@ private Q_SLOTS: void onClearTriggered(); void onSaveTriggered(); void appendMessage(const QLatin1StringView& channel, quint32 cat, const QString& message); + void realAppendMessage(const QLatin1StringView& channel, quint32 cat, const QString& message); private: static constexpr int DEFAULT_WIDTH = 750; static constexpr int DEFAULT_HEIGHT = 400; + static constexpr int MAX_LINES = 1000; + static constexpr int BLOCK_UPDATES_THRESHOLD = 100; void saveSize(); void restoreSize(); @@ -54,9 +59,13 @@ private Q_SLOTS: QPlainTextEdit* m_text; QMenu* m_level_menu; + int m_lines_to_skip = 0; + bool m_is_dark_theme = false; bool m_attached_to_main_window = true; bool m_destroying = false; + + ALIGN_TO_CACHE_LINE std::atomic_int m_lines_pending{0}; }; extern LogWindow* g_log_window; From e03631855928df5344af0bc91ac8a2781a5c80ea Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 2 Jan 2025 00:31:15 +1000 Subject: [PATCH 15/22] Qt: Add 'Controller Test' to tools menu --- src/duckstation-qt/mainwindow.cpp | 1 + src/duckstation-qt/mainwindow.ui | 6 +++++ src/duckstation-qt/qthost.cpp | 39 +++++++++++++++++++++++++++++++ src/duckstation-qt/qthost.h | 1 + 4 files changed, 47 insertions(+) diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index bb46b26c9e..297fd77bbb 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -2026,6 +2026,7 @@ void MainWindow::connectSignals() connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered); connect(m_ui.actionISOBrowser, &QAction::triggered, this, &MainWindow::onToolsISOBrowserTriggered); connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered); + connect(m_ui.actionControllerTest, &QAction::triggered, g_emu_thread, &EmuThread::startControllerTest); connect(m_ui.actionMediaCapture, &QAction::toggled, this, &MainWindow::onToolsMediaCaptureToggled); connect(m_ui.actionCaptureGPUFrame, &QAction::triggered, g_emu_thread, &EmuThread::captureGPUFrameDump); connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger); diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 42b5997ed1..ac8a0ebbb3 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -227,6 +227,7 @@ + @@ -979,6 +980,11 @@ Free Camera + + + Controller Test + + diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 367ae3e875..2282b9cced 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1334,6 +1334,45 @@ void EmuThread::captureGPUFrameDump() System::StartRecordingGPUDump(); } +void EmuThread::startControllerTest() +{ + static constexpr const char* PADTEST_URL = "https://downloads.duckstation.org/runtime-resources/padtest.psexe"; + + if (!isCurrentThread()) + { + QMetaObject::invokeMethod(this, "startControllerTest", Qt::QueuedConnection); + return; + } + + if (System::IsValid()) + return; + + std::string path = Path::Combine(EmuFolders::UserResources, "padtest.psexe"); + if (FileSystem::FileExists(path.c_str())) + { + bootSystem(std::make_shared(std::move(path))); + return; + } + + QtHost::RunOnUIThread([path = std::move(path)]() mutable { + { + auto lock = g_main_window->pauseAndLockSystem(); + if (QMessageBox::question( + lock.getDialogParent(), tr("Confirm Download"), + tr("Your DuckStation installation does not have the padtest application " + "available.\n\nThis file is approximately 206KB, do you want to download it now?")) != QMessageBox::Yes) + { + return; + } + + if (!QtHost::DownloadFile(lock.getDialogParent(), tr("File Download"), PADTEST_URL, path.c_str())) + return; + } + + g_emu_thread->startControllerTest(); + }); +} + void EmuThread::runOnEmuThread(std::function callback) { callback(); diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index b3917649c6..9f1f043753 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -212,6 +212,7 @@ public Q_SLOTS: void clearInputBindStateFromSource(InputBindingKey key); void reloadTextureReplacements(); void captureGPUFrameDump(); + void startControllerTest(); void setGPUThreadRunIdle(bool active); private Q_SLOTS: From 0fdf984b71ab9b5f45f7675059865aeef0a46e82 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 2 Jan 2025 00:38:27 +1000 Subject: [PATCH 16/22] GTE: Disable freecam on Android Freecam is disabled on Android because there's no windowed UI for it. And because users can't be trusted to not crash games and complain. --- src/core/gte.cpp | 58 +++++++++++++++++++++++++++++++++++++++++--- src/core/hotkeys.cpp | 5 ++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/core/gte.cpp b/src/core/gte.cpp index 9ff322dff3..26ee705cfe 100644 --- a/src/core/gte.cpp +++ b/src/core/gte.cpp @@ -25,6 +25,12 @@ LOG_CHANNEL(Host); +// Freecam is disabled on Android because there's no windowed UI for it. +// And because users can't be trusted to not crash games and complain. +#ifndef __ANDROID__ +#define ENABLE_FREECAM 1 +#endif + namespace GTE { static constexpr s64 MAC0_MIN_VALUE = -(INT64_C(1) << 31); @@ -47,14 +53,14 @@ static constexpr float FREECAM_MAX_TURN_SPEED = 360.0f; namespace { -struct Config +struct ALIGN_TO_CACHE_LINE Config { DisplayAspectRatio aspect_ratio = DisplayAspectRatio::R4_3; u32 custom_aspect_ratio_numerator; u32 custom_aspect_ratio_denominator; float custom_aspect_ratio_f; - ////////////////////////////////////////////////////////////////////////// +#ifdef ENABLE_FREECAM Timer::Value freecam_update_time = 0; std::atomic_bool freecam_transform_changed{false}; @@ -70,11 +76,13 @@ struct Config GSVector4 freecam_translation = GSVector4::cxpr(0.0f); ALIGN_TO_CACHE_LINE GSMatrix4x4 freecam_matrix = GSMatrix4x4::Identity(); + +#endif }; } // namespace -ALIGN_TO_CACHE_LINE static Config s_config; +static Config s_config; #define REGS CPU::g_state.gte_regs @@ -209,7 +217,6 @@ static void PushSZ(s32 value); static void PushRGBFromMAC(); static u32 UNRDivide(u32 lhs, u32 rhs); -static void ApplyFreecam(s64& x, s64& y, s64& z); static void MulMatVec(const s16* M_, const s16 Vx, const s16 Vy, const s16 Vz, u8 shift, bool lm); static void MulMatVec(const s16* M_, const s32 T[3], const s16 Vx, const s16 Vy, const s16 Vz, u8 shift, bool lm); static void MulMatVecBuggy(const s16* M_, const s32 T[3], const s16 Vx, const s16 Vy, const s16 Vz, u8 shift, bool lm); @@ -221,6 +228,10 @@ static void NCCS(const s16 V[3], u8 shift, bool lm); static void NCDS(const s16 V[3], u8 shift, bool lm); static void DPCS(const u8 color[3], u8 shift, bool lm); +#ifdef ENABLE_FREECAM +static void ApplyFreecam(s64& x, s64& y, s64& z); +#endif + static void Execute_MVMVA(Instruction inst); static void Execute_SQR(Instruction inst); static void Execute_OP(Instruction inst); @@ -687,8 +698,12 @@ void GTE::RTPS(const s16 V[3], u8 shift, bool lm, bool last) s64 x = dot3(0); s64 y = dot3(1); s64 z = dot3(2); + +#ifdef ENABLE_FREECAM if (s_config.freecam_active) ApplyFreecam(x, y, z); +#endif + TruncateAndSetMAC<1>(x, shift); TruncateAndSetMAC<2>(y, shift); TruncateAndSetMAC<3>(z, shift); @@ -1416,6 +1431,8 @@ GTE::InstructionImpl GTE::GetInstructionImpl(u32 inst_bits, TickCount* ticks) } } +#ifdef ENABLE_FREECAM + bool GTE::IsFreecamEnabled() { return s_config.freecam_enabled; @@ -1681,3 +1698,36 @@ void GTE::DrawFreecamWindow(float scale) if (changed) s_config.freecam_transform_changed.store(true, std::memory_order_release); } + +#else // ENABLE_FREECAM + +bool GTE::IsFreecamEnabled() +{ + return false; +} + +void GTE::SetFreecamEnabled(bool enabled) +{ +} + +void GTE::SetFreecamMoveAxis(u32 axis, float x) +{ +} + +void GTE::SetFreecamRotateAxis(u32 axis, float x) +{ +} + +void GTE::UpdateFreecam(u64 current_time) +{ +} + +void GTE::ResetFreecam() +{ +} + +void GTE::DrawFreecamWindow(float scale) +{ +} + +#endif // ENABLE_FREECAM diff --git a/src/core/hotkeys.cpp b/src/core/hotkeys.cpp index c09c481bc5..565c3931d0 100644 --- a/src/core/hotkeys.cpp +++ b/src/core/hotkeys.cpp @@ -521,6 +521,9 @@ DEFINE_HOTKEY("RotateCounterclockwise", TRANSLATE_NOOP("Hotkeys", "Graphics"), } }) +// See gte.cpp. +#ifndef __ANDROID__ + DEFINE_HOTKEY("FreecamToggle", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Freecam Toggle"), [](s32 pressed) { if (!pressed && !Achievements::IsHardcoreModeActive()) @@ -616,6 +619,8 @@ DEFINE_HOTKEY("FreecamRollRight", TRANSLATE_NOOP("Hotkeys", "Graphics"), GTE::SetFreecamRotateAxis(2, std::max(static_cast(pressed), 0.0f)); }) +#endif // __ANDROID__ + DEFINE_HOTKEY("AudioMute", TRANSLATE_NOOP("Hotkeys", "Audio"), TRANSLATE_NOOP("Hotkeys", "Toggle Mute"), [](s32 pressed) { if (!pressed && System::IsValid()) From 179e2f19993f373cc94630b6f244463fd125e7d9 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 2 Jan 2025 00:52:34 +1000 Subject: [PATCH 17/22] FullscrenUI: Fix field spacing scaling in game list view --- src/core/fullscreen_ui.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 5ac2317420..81f56ef7b9 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -6929,14 +6929,14 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) } const float work_width = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); - static constexpr float field_margin_y = 20.0f; + static constexpr float field_margin_y = 4.0f; static constexpr float start_x = 50.0f; float text_y = info_top_margin + cover_size + info_top_margin; float text_width; PushPrimaryColor(); ImGui::SetCursorPos(LayoutScale(start_x, text_y)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, field_margin_y)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(0.0f, field_margin_y)); ImGui::BeginGroup(); if (selected_entry) @@ -6967,7 +6967,7 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) text_width = ImGui::CalcTextSize(selected_entry->serial.c_str(), nullptr, false, work_width).x; ImGui::SetCursorPosX((work_width - text_width) / 2.0f); ImGui::TextWrapped("%s", selected_entry->serial.c_str()); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 15.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(15.0f)); // region { From b21312867bfd91e374d936fdf9b931831c3b251e Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 2 Jan 2025 01:07:04 +1000 Subject: [PATCH 18/22] FullscreenUI: Fix popup sizes for postfx settings --- src/core/fullscreen_ui.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 81f56ef7b9..7395a5b403 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -4876,7 +4876,7 @@ void FullscreenUI::DrawPostProcessingSettingsPage() if (MenuButton(tstr, str)) ImGui::OpenPopup(tstr); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 190.0f)); + ImGui::SetNextWindowSize(LayoutScale(500.0f, 194.0f)); ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::PushFont(UIStyle.LargeFont); @@ -4884,6 +4884,7 @@ void FullscreenUI::DrawPostProcessingSettingsPage() ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); bool is_open = true; if (ImGui::BeginPopupModal(tstr, &is_open, @@ -4962,7 +4963,7 @@ void FullscreenUI::DrawPostProcessingSettingsPage() ImGui::EndPopup(); } - ImGui::PopStyleVar(3); + ImGui::PopStyleVar(4); ImGui::PopFont(); } break; @@ -4975,7 +4976,7 @@ void FullscreenUI::DrawPostProcessingSettingsPage() if (MenuButton(tstr, str)) ImGui::OpenPopup(tstr); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 190.0f)); + ImGui::SetNextWindowSize(LayoutScale(500.0f, 194.0f)); ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::PushFont(UIStyle.LargeFont); @@ -4983,6 +4984,7 @@ void FullscreenUI::DrawPostProcessingSettingsPage() ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); bool is_open = true; if (ImGui::BeginPopupModal(tstr, &is_open, @@ -5060,7 +5062,7 @@ void FullscreenUI::DrawPostProcessingSettingsPage() ImGui::EndPopup(); } - ImGui::PopStyleVar(3); + ImGui::PopStyleVar(4); ImGui::PopFont(); } break; From 3a64c5e4b37517eee165d9af10a252d13ec23967 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 2 Jan 2025 01:01:34 +1000 Subject: [PATCH 19/22] FullscreenUI: Improve field alignment in achievements login --- src/core/fullscreen_ui.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 7395a5b403..1e0b506e0c 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "fullscreen_ui.h" @@ -5340,17 +5340,33 @@ void FullscreenUI::DrawAchievementsLoginWindow() const bool is_logging_in = ImGuiFullscreen::IsBackgroundProgressDialogOpen(LOGIN_PROGRESS_NAME); ResetFocusHere(); + const float inner_spacing = LayoutScale(6.0f); + const float item_margin = LayoutScale(10.0f); + const float item_width = LayoutScale(550.0f); + ImGui::Columns(2, "LoginFields", true); + ImGui::SetColumnWidth(0, LayoutScale(150.0f)); + ImGui::SetColumnWidth(1, item_width); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + inner_spacing); ImGui::Text(FSUI_CSTR("User Name: ")); - ImGui::SameLine(LayoutScale(200.0f)); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(item_width); ImGui::InputText("##username", username, sizeof(username), is_logging_in ? ImGuiInputTextFlags_ReadOnly : 0); + ImGui::NextColumn(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_margin + inner_spacing); ImGui::Text(FSUI_CSTR("Password: ")); - ImGui::SameLine(LayoutScale(200.0f)); + ImGui::NextColumn(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_margin); + ImGui::SetNextItemWidth(item_width); ImGui::InputText("##password", password, sizeof(password), is_logging_in ? (ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_Password) : ImGuiInputTextFlags_Password); + ImGui::NextColumn(); - ImGui::NewLine(); + ImGui::Columns(1); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_margin); const bool login_enabled = (std::strlen(username) > 0 && std::strlen(password) > 0 && !is_logging_in); From cd873eb6c18ab3db0dbff8123e9a196b2f1ebd63 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 2 Jan 2025 02:08:37 +1000 Subject: [PATCH 20/22] GTE: Add 'Reverse Transform Order' option to freecam --- src/core/gte.cpp | 84 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/src/core/gte.cpp b/src/core/gte.cpp index 26ee705cfe..cc95942258 100644 --- a/src/core/gte.cpp +++ b/src/core/gte.cpp @@ -66,6 +66,7 @@ struct ALIGN_TO_CACHE_LINE Config std::atomic_bool freecam_transform_changed{false}; bool freecam_enabled = false; bool freecam_active = false; + bool freecam_reverse_transform_order = false; float freecam_move_speed = FREECAM_DEFAULT_MOVE_SPEED; float freecam_turn_speed = FREECAM_DEFAULT_TURN_SPEED; @@ -1511,30 +1512,61 @@ void GTE::UpdateFreecam(u64 current_time) // translate than rotate, since the camera is rotating around a point // remember, matrix transformation happens in the opposite of the multiplication order - if (s_config.freecam_translation.x != 0.0f || s_config.freecam_translation.y != 0.0f || - s_config.freecam_translation.z != 0.0f) + if (!s_config.freecam_reverse_transform_order) { - s_config.freecam_matrix = GSMatrix4x4::Translation(s_config.freecam_translation.x, s_config.freecam_translation.y, - s_config.freecam_translation.z); - any_xform = true; - } + if (s_config.freecam_translation.x != 0.0f || s_config.freecam_translation.y != 0.0f || + s_config.freecam_translation.z != 0.0f) + { + s_config.freecam_matrix = GSMatrix4x4::Translation(s_config.freecam_translation.x, s_config.freecam_translation.y, + s_config.freecam_translation.z); + any_xform = true; + } - if (s_config.freecam_rotation.z != 0.0f) - { - s_config.freecam_matrix *= GSMatrix4x4::RotationZ(s_config.freecam_rotation.z); - any_xform = true; - } + if (s_config.freecam_rotation.z != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::RotationZ(s_config.freecam_rotation.z); + any_xform = true; + } - if (s_config.freecam_rotation.y != 0.0f) - { - s_config.freecam_matrix *= GSMatrix4x4::RotationY(s_config.freecam_rotation.y); - any_xform = true; - } + if (s_config.freecam_rotation.y != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::RotationY(s_config.freecam_rotation.y); + any_xform = true; + } - if (s_config.freecam_rotation.x != 0.0f) + if (s_config.freecam_rotation.x != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::RotationX(s_config.freecam_rotation.x); + any_xform = true; + } + } + else { - s_config.freecam_matrix *= GSMatrix4x4::RotationX(s_config.freecam_rotation.x); - any_xform = true; + if (s_config.freecam_rotation.x != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::RotationX(s_config.freecam_rotation.x); + any_xform = true; + } + + if (s_config.freecam_rotation.y != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::RotationY(s_config.freecam_rotation.y); + any_xform = true; + } + + if (s_config.freecam_rotation.z != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::RotationZ(s_config.freecam_rotation.z); + any_xform = true; + } + + if (s_config.freecam_translation.x != 0.0f || s_config.freecam_translation.y != 0.0f || + s_config.freecam_translation.z != 0.0f) + { + s_config.freecam_matrix *= GSMatrix4x4::Translation( + s_config.freecam_translation.x, s_config.freecam_translation.y, s_config.freecam_translation.z); + any_xform = true; + } } s_config.freecam_active = any_xform; @@ -1565,14 +1597,14 @@ void GTE::ApplyFreecam(s64& x, s64& y, s64& z) void GTE::DrawFreecamWindow(float scale) { const ImGuiStyle& style = ImGui::GetStyle(); - - bool freecam_enabled = s_config.freecam_enabled; - bool enabled_changed = false; - const float label_width = 140.0f * scale; const float item_width = 350.0f * scale; const float padding_height = 5.0f * scale; + bool freecam_enabled = s_config.freecam_enabled; + bool enabled_changed = false; + bool changed = false; + if (ImGui::CollapsingHeader("Settings", ImGuiTreeNodeFlags_DefaultOpen)) { const float third_width = 50.0f * scale; @@ -1580,6 +1612,10 @@ void GTE::DrawFreecamWindow(float scale) enabled_changed = ImGui::Checkbox("Enable Freecam", &freecam_enabled); + changed |= ImGui::Checkbox("Reverse Transform Order", &s_config.freecam_reverse_transform_order); + ImGui::SetItemTooltip("Swaps the order that the camera rotation/offset is applied.\nCan work better in some games " + "that use different modelview matrices."); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padding_height); ImGui::Columns(3, "Settings", false); @@ -1611,8 +1647,6 @@ void GTE::DrawFreecamWindow(float scale) ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padding_height); } - bool changed = false; - if (ImGui::CollapsingHeader("Rotation", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Columns(2, "Rotation", false); From 08cd649187522c4fbaed6f41f5c0f7c69cb9c5b9 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 2 Jan 2025 02:09:43 +1000 Subject: [PATCH 21/22] InputManager: Fix pointer-bound bind movement i.e. psmouse Regression from c4e0e7fadea0bf4f9da7bdea4335f619a6f14385 --- src/util/input_manager.cpp | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/util/input_manager.cpp b/src/util/input_manager.cpp index 5a12eb978d..0d8eeda160 100644 --- a/src/util/input_manager.cpp +++ b/src/util/input_manager.cpp @@ -1223,27 +1223,29 @@ void InputManager::GenerateRelativeMouseEvents() for (u32 axis = 0; axis < static_cast(static_cast(InputPointerAxis::Count)); axis++) { PointerAxisState& state = s_pointer_state[device][axis]; - const float delta = static_cast(state.delta.exchange(0, std::memory_order_acquire)) / 65536.0f; + const int deltai = state.delta.load(std::memory_order_acquire); + state.delta.fetch_sub(deltai, std::memory_order_release); + const float delta = static_cast(deltai) / 65536.0f; const float unclamped_value = delta * s_pointer_axis_scale[axis]; const float value = std::clamp(unclamped_value, -1.0f, 1.0f); - if (value == state.last_value) - continue; - - state.last_value = value; const InputBindingKey key(MakePointerAxisKey(device, static_cast(axis))); - if (device == 0 && axis >= static_cast(InputPointerAxis::WheelX) && - ImGuiManager::ProcessPointerAxisEvent(key, unclamped_value)) + if (device == 0 && axis >= static_cast(InputPointerAxis::WheelX) && delta != 0.0f && + ImGuiManager::ProcessPointerAxisEvent(key, delta)) { continue; } - if (!system_running) - continue; - - InvokeEvents(key, value, GenericInputBinding::Unknown); + // only generate axis-bound events when it hasn't changed + if (value != state.last_value) + { + state.last_value = value; + if (system_running) + InvokeEvents(key, value, GenericInputBinding::Unknown); + } - if (delta != 0.0f) + // and pointer events only when it hasn't moved + if (delta != 0.0f && system_running) { for (const std::pair& pmc : s_pointer_move_callbacks) { From 83b47577880e65de18c2d8a72b5371496783938e Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 2 Jan 2025 02:11:50 +1000 Subject: [PATCH 22/22] Qt: Controller Test should be disabled while running --- src/duckstation-qt/mainwindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 297fd77bbb..c87c496557 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -1741,6 +1741,8 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevo m_ui.actionViewGameProperties->setDisabled(starting_or_not_running); + m_ui.actionControllerTest->setDisabled(starting_or_running); + updateShortcutActions(starting); if (starting_or_running)