diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp index 18c573fcb2..3dcd94ef10 100644 --- a/Common/UI/View.cpp +++ b/Common/UI/View.cpp @@ -852,6 +852,11 @@ void CheckBox::GetContentDimensions(const UIContext &dc, float &w, float &h) con w = bounds_.w; } +BitCheckBox::BitCheckBox(uint32_t *bitfield, uint32_t bit, std::string_view text, std::string_view smallText, LayoutParams *layoutParams) + : CheckBox(nullptr, text, smallText, layoutParams), bitfield_(bitfield), bit_(bit) { + _dbg_assert_msg_(bit != 0, "bit is a mask, not a bit index"); +} + void BitCheckBox::Toggle() { if (bitfield_) { *bitfield_ = *bitfield_ ^ bit_; diff --git a/Common/UI/View.h b/Common/UI/View.h index 2372d1ad06..42af14edab 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -916,10 +916,8 @@ private: class BitCheckBox : public CheckBox { public: - BitCheckBox(uint32_t *bitfield, uint32_t bit, std::string_view text, std::string_view smallText = "", LayoutParams *layoutParams = nullptr) - : CheckBox(nullptr, text, smallText, layoutParams), bitfield_(bitfield), bit_(bit) { - } - + // bit is a bitmask (should only have a single bit set), not a bit index. + BitCheckBox(uint32_t *bitfield, uint32_t bit, std::string_view text, std::string_view smallText = "", LayoutParams *layoutParams = nullptr); BitCheckBox(int *bitfield, int bit, std::string_view text, std::string_view smallText = "", LayoutParams *layoutParams = nullptr) : BitCheckBox((uint32_t *)bitfield, (uint32_t)bit, text, smallText, layoutParams) {} void Toggle() override; diff --git a/Core/Config.cpp b/Core/Config.cpp index ae19a040e0..6bdbf8e06c 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -306,6 +306,8 @@ static const ConfigSetting generalSettings[] = { ConfigSetting("IgnoreCompatSettings", &g_Config.sIgnoreCompatSettings, "", CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("RunBehindPauseMenu", &g_Config.bRunBehindPauseMenu, false, CfgFlag::DEFAULT), + + ConfigSetting("ShowGPOLEDs", &g_Config.bShowGPOLEDs, false, CfgFlag::PER_GAME), }; static bool DefaultSasThread() { diff --git a/Core/Config.h b/Core/Config.h index f6addf3f1c..61a30c5d5a 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -265,6 +265,9 @@ public: bool bRenderDuplicateFrames; bool bRenderMultiThreading; + // HW debug + bool bShowGPOLEDs; + // Sound bool bEnableSound; int iAudioBackend; diff --git a/Core/HLE/sceKernel.cpp b/Core/HLE/sceKernel.cpp index 28d6a60a0b..d4da98bf42 100644 --- a/Core/HLE/sceKernel.cpp +++ b/Core/HLE/sceKernel.cpp @@ -100,6 +100,8 @@ static bool kernelRunning = false; KernelObjectPool kernelObjects; KernelStats kernelStats; u32 registeredExitCbId; +u32 g_GPOBits; // Really just 8 bits on the real hardware. +u32 g_GPIBits; // Really just 8 bits on the real hardware. void __KernelInit() { @@ -161,6 +163,7 @@ void __KernelInit() __PPGeInit(); kernelRunning = true; + g_GPOBits = 0; INFO_LOG(SCEKERNEL, "Kernel initialized."); } @@ -355,18 +358,19 @@ int sceKernelRegisterDefaultExceptionHandler() return 0; } -void sceKernelSetGPO(u32 ledAddr) +void sceKernelSetGPO(u32 ledBits) { - // Sets debug LEDs. - // Not really interesting, and a few games really spam it - // DEBUG_LOG(SCEKERNEL, "sceKernelSetGPO(%02x)", ledAddr); + // Sets debug LEDs. Some games do interesting stuff with this, like a metronome in Parappa. + // Shows up as a vertical strip of LEDs at the side of the screen, if enabled. + g_GPOBits = ledBits; } u32 sceKernelGetGPI() { // Always returns 0 on production systems. - DEBUG_LOG(SCEKERNEL, "0=sceKernelGetGPI()"); - return 0; + // On developer systems, there are 8 switches that control the lower 8 bits of the return value. + DEBUG_LOG(SCEKERNEL, "%d=sceKernelGetGPI()", g_GPIBits); + return g_GPIBits; } // #define LOG_CACHE diff --git a/Core/HLE/sceKernel.h b/Core/HLE/sceKernel.h index 722f89d37d..4c03d5848b 100644 --- a/Core/HLE/sceKernel.h +++ b/Core/HLE/sceKernel.h @@ -569,6 +569,9 @@ struct KernelStats { extern KernelStats kernelStats; extern u32 registeredExitCbId; +extern u32 g_GPOBits; +extern u32 g_GPIBits; + void Register_ThreadManForUser(); void Register_ThreadManForKernel(); void Register_LoadExecForUser(); diff --git a/UI/DevScreens.cpp b/UI/DevScreens.cpp index 8da4308886..f16dbfef01 100644 --- a/UI/DevScreens.cpp +++ b/UI/DevScreens.cpp @@ -56,6 +56,7 @@ #include "Core/System.h" #include "Core/Reporting.h" #include "Core/CoreParameter.h" +#include "Core/HLE/sceKernel.h" // GPI/GPO #include "Core/MIPS/MIPSTables.h" #include "Core/MIPS/JitCommon/JitBlockCache.h" #include "Core/MIPS/JitCommon/JitCommon.h" @@ -141,6 +142,11 @@ void DevMenuScreen::CreatePopupContents(UI::ViewGroup *parent) { items->Add(new Choice(dev->T("Reset limited logging")))->OnClick.Handle(this, &DevMenuScreen::OnResetLimitedLogging); + items->Add(new Choice(dev->T("GPI/GPO switches/LEDs")))->OnClick.Add([=](UI::EventParams &e) { + screenManager()->push(new GPIGPOScreen(dev->T("GPI/GPO switches/LEDs"))); + return UI::EVENT_DONE; + }); + items->Add(new Choice(dev->T("Create frame dump")))->OnClick.Add([](UI::EventParams &e) { GPURecord::RecordNextFrame([](const Path &dumpPath) { NOTICE_LOG(SYSTEM, "Frame dump created at '%s'", dumpPath.c_str()); @@ -213,6 +219,16 @@ void DevMenuScreen::dialogFinished(const Screen *dialog, DialogResult result) { // TriggerFinish(DR_OK); } +void GPIGPOScreen::CreatePopupContents(UI::ViewGroup *parent) { + using namespace UI; + auto dev = GetI18NCategory(I18NCat::DEVELOPER); + parent->Add(new CheckBox(&g_Config.bShowGPOLEDs, dev->T("Show GPO LEDs"))); + for (int i = 0; i < 8; i++) { + std::string name = ApplySafeSubstitutions(dev->T("GPI switch %1"), i); + parent->Add(new BitCheckBox(&g_GPIBits, 1 << i, name)); + } +} + void LogScreen::UpdateLog() { using namespace UI; RingbufferLogListener *ring = LogManager::GetInstance()->GetRingbufferListener(); diff --git a/UI/DevScreens.h b/UI/DevScreens.h index 29fbf5690b..9ae176a840 100644 --- a/UI/DevScreens.h +++ b/UI/DevScreens.h @@ -145,6 +145,15 @@ private: unsigned int addr_; }; +class GPIGPOScreen : public PopupScreen { +public: + GPIGPOScreen(std::string_view title) : PopupScreen(title, "OK") {} + const char *tag() const override { return "GPIGPO"; } + +protected: + void CreatePopupContents(UI::ViewGroup *parent) override; +}; + class JitCompareScreen : public UIDialogScreenWithBackground { public: void CreateViews() override; diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index da7d4f278f..eafa4c9fde 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1521,6 +1521,8 @@ bool EmuScreen::hasVisibleUI() { return true; if (g_Config.bEnableCardboardVR || g_Config.bEnableNetworkChat) return true; + if (g_Config.bShowGPOLEDs) + return true; // Debug UI. if ((DebugOverlay)g_Config.iDebugOverlay != DebugOverlay::OFF || g_Config.bShowDeveloperMenu) return true; @@ -1571,6 +1573,26 @@ void EmuScreen::renderUI() { } #endif + if (g_Config.bShowGPOLEDs) { + // Draw a vertical strip of LEDs at the right side of the screen. + const float ledSize = 24.0f; + const float spacing = 4.0f; + const float height = 8 * ledSize + 7 * spacing; + const float x = ctx->GetBounds().w - spacing - ledSize; + const float y = (ctx->GetBounds().h - height) * 0.5f; + ctx->FillRect(UI::Drawable(0xFF000000), Bounds(x - spacing, y - spacing, ledSize + spacing * 2, height + spacing * 2)); + for (int i = 0; i < 8; i++) { + int bit = (g_GPOBits >> i) & 1; + uint32_t color = 0xFF30FF30; + if (!bit) { + color = darkenColor(darkenColor(color)); + } + Bounds ledBounds(x, y + (spacing + ledSize) * i, ledSize, ledSize); + ctx->FillRect(UI::Drawable(color), ledBounds); + } + ctx->Flush(); + } + if (coreState == CORE_RUNTIME_ERROR || coreState == CORE_STEPPING) { const MIPSExceptionInfo &info = Core_GetExceptionInfo(); if (info.type != MIPSExceptionType::NONE) { diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 40283066fe..5dc9404dd7 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -1843,6 +1843,11 @@ void DeveloperToolsScreen::CreateViews() { list->Add(new CheckBox(&g_Config.bShowOnScreenMessages, dev->T("Show on-screen messages"))); + list->Add(new Choice(dev->T("GPI/GPO switches/LEDs")))->OnClick.Add([=](UI::EventParams &e) { + screenManager()->push(new GPIGPOScreen(dev->T("GPI/GPO switches/LEDs"))); + return UI::EVENT_DONE; + }); + #if PPSSPP_PLATFORM(ANDROID) static const char *framerateModes[] = { "Default", "Request 60Hz", "Force 60Hz" }; PopupMultiChoice *framerateMode = list->Add(new PopupMultiChoice(&g_Config.iDisplayFramerateMode, gr->T("Framerate mode"), framerateModes, 0, ARRAY_SIZE(framerateModes), I18NCat::GRAPHICS, screenManager()));