From 86c42a00a3cc4b9de54c1129375695e2ead59998 Mon Sep 17 00:00:00 2001 From: RedPanda4552 Date: Thu, 23 Nov 2023 23:15:38 -0500 Subject: [PATCH] Pad: Add controller ejection --- pcsx2/SIO/Pad/Pad.cpp | 80 ++++++++++++++++++++++--------- pcsx2/SIO/Pad/Pad.h | 2 + pcsx2/SIO/Pad/PadBase.cpp | 3 +- pcsx2/SIO/Pad/PadBase.h | 10 +++- pcsx2/SIO/Pad/PadDualshock2.cpp | 4 +- pcsx2/SIO/Pad/PadDualshock2.h | 2 +- pcsx2/SIO/Pad/PadGuitar.cpp | 4 +- pcsx2/SIO/Pad/PadGuitar.h | 2 +- pcsx2/SIO/Pad/PadNotConnected.cpp | 4 +- pcsx2/SIO/Pad/PadNotConnected.h | 2 +- pcsx2/SIO/Sio2.cpp | 28 +++++++++-- 11 files changed, 102 insertions(+), 39 deletions(-) diff --git a/pcsx2/SIO/Pad/Pad.cpp b/pcsx2/SIO/Pad/Pad.cpp index be87a99e06c3c..e8a0ece433c5a 100644 --- a/pcsx2/SIO/Pad/Pad.cpp +++ b/pcsx2/SIO/Pad/Pad.cpp @@ -25,6 +25,7 @@ #include "IconsFontAwesome5.h" +#include "VMManager.h" #include "common/Assertions.h" #include "common/FileSystem.h" #include "common/Path.h" @@ -57,6 +58,9 @@ namespace Pad static std::array, NUM_CONTROLLER_PORTS> s_macro_buttons; static std::array, NUM_CONTROLLER_PORTS> s_controllers; + + bool mtapPort0LastState; + bool mtapPort1LastState; } bool Pad::Initialize() @@ -90,19 +94,24 @@ void Pad::LoadConfig(const SettingsInterface& si) { s_macro_buttons = {}; + const bool mtapPort0Changed = EmuConfig.Pad.MultitapPort0_Enabled != Pad::mtapPort0LastState; + const bool mtapPort1Changed = EmuConfig.Pad.MultitapPort1_Enabled != Pad::mtapPort1LastState; + for (u32 i = 0; i < Pad::NUM_CONTROLLER_PORTS; i++) { const std::string section = GetConfigSection(i); const ControllerInfo* ci = GetControllerInfo(EmuConfig.Pad.Ports[i].Type); pxAssert(ci); - // If a pad is not yet constructed, at minimum place a NotConnected pad in the slot. - // Do not abort the for loop - If there are pad settings, we want those to be applied to the slot. PadBase* pad = Pad::GetPad(i); - if (!pad || pad->GetType() != ci->type) + // If pad pointer is not occupied yet, type in settings no longer matches the current type, or a multitap was slotted in/out, + // then reconstruct a new pad. + if (!pad || pad->GetType() != ci->type || (mtapPort0Changed && (i <= 4 && i != 1)) || (mtapPort1Changed && (i >= 5 || i == 1))) { - pad = Pad::CreatePad(i, ci->type); + // Create the new pad. If the VM is in any kind of running state at all, set eject ticks so the PS2 will think + // there was some kind of pad ejection event and properly detect the new one, and properly initiate its config sequence. + pad = Pad::CreatePad(i, ci->type, (VMManager::GetState() != VMState::Shutdown ? Pad::DEFAULT_EJECT_TICKS : 0)); pxAssert(pad); } @@ -129,6 +138,9 @@ void Pad::LoadConfig(const SettingsInterface& si) pad->SetAnalogInvertR((invert_r & 1) != 0, (invert_r & 2) != 0); LoadMacroButtonConfig(si, i, ci, section); } + + Pad::mtapPort0LastState = EmuConfig.Pad.MultitapPort0_Enabled; + Pad::mtapPort1LastState = EmuConfig.Pad.MultitapPort1_Enabled; } Pad::ControllerType Pad::GetDefaultPadType(u32 pad) @@ -526,35 +538,57 @@ bool Pad::Freeze(StateWrapper& sw) for (u32 unifiedSlot = 0; unifiedSlot < NUM_CONTROLLER_PORTS; unifiedSlot++) { - ControllerType type; - sw.Do(&type); + PadBase* currentPad = GetPad(unifiedSlot); + ControllerType statePadType; + + sw.Do(&statePadType); + if (sw.HasError()) return false; - - PadBase* tempPad; - PadBase* pad = GetPad(unifiedSlot); - if (!pad || pad->GetType() != type) + + if (!currentPad) + { + pxAssertMsg(false, fmt::format("Pad::Freeze (on read) Existing Pad {0} was nullptr", unifiedSlot).c_str()); + } + // If the currently configured pad is of a different type than the pad which was used during the savestate... + else if (currentPad->GetType() != statePadType) { + const ControllerType currentPadType = currentPad->GetType(); + const auto& [port, slot] = sioConvertPadToPortAndSlot(unifiedSlot); Host::AddIconOSDMessage(fmt::format("UnfreezePad{}Changed", unifiedSlot), ICON_FA_GAMEPAD, fmt::format(TRANSLATE_FS("Pad", "Controller port {0}, slot {1} has a {2} connected, but the save state has a " - "{3}.\nLeaving the original controller type connected, but this may cause issues."), + "{3}.\nEjecting {3} and replacing it with {2}."), port, slot, - GetControllerTypeName(pad ? pad->GetType() : Pad::ControllerType::NotConnected), - GetControllerTypeName(type))); - - // Reset the transfer etc state of the pad, at least it has a better chance of surviving. - if (pad) - pad->SoftReset(); - - // But we still need to pull the data from the state.. - tempPad = CreatePad(unifiedSlot, type); - pad = tempPad; + GetControllerTypeName(currentPad ? currentPad->GetType() : Pad::ControllerType::NotConnected), + GetControllerTypeName(statePadType))); + + // Run the freeze, using a new pad instance of the old type just so we make sure all those attributes + // from the state are read out and we aren't going to run into some sort of consistency problem. + currentPad = CreatePad(unifiedSlot, statePadType); + + if (currentPad) + { + currentPad->Freeze(sw); + + // Now immediately discard whatever malformed pad state we just created, and replace it with a fresh pad loaded + // using whatever the current user settings are. Savestates are, by definition, never going to occur in the middle + // of a transfer between SIO2 and the peripheral, since they aren't captured until the VM is at a point where everything + // is "stoppable". For all intents and purposes, by the time a savestate is captured, the IOP is "done" and there is no + // "pending work" left hanging in SIO2 or the pads. So there is nothing actually lost from just throwing the pad away and making a new one here. + currentPad = CreatePad(unifiedSlot, currentPadType, Pad::DEFAULT_EJECT_TICKS); + } + else + { + pxAssertMsg(false, fmt::format("Pad::Freeze (on read) State Pad {0} was nullptr", unifiedSlot).c_str()); + } } - - if (!pad->Freeze(sw)) + // ... else, just run the freeze normally. + else if (currentPad && !currentPad->Freeze(sw)) + { return false; + } } } else diff --git a/pcsx2/SIO/Pad/Pad.h b/pcsx2/SIO/Pad/Pad.h index fb91029ebbeab..8f65206747e3c 100644 --- a/pcsx2/SIO/Pad/Pad.h +++ b/pcsx2/SIO/Pad/Pad.h @@ -28,6 +28,8 @@ class StateWrapper; namespace Pad { + constexpr size_t DEFAULT_EJECT_TICKS = 50; + bool Initialize(); void Shutdown(); diff --git a/pcsx2/SIO/Pad/PadBase.cpp b/pcsx2/SIO/Pad/PadBase.cpp index 861a5e7fd4ffd..a16353a94fc91 100644 --- a/pcsx2/SIO/Pad/PadBase.cpp +++ b/pcsx2/SIO/Pad/PadBase.cpp @@ -17,9 +17,10 @@ #include "SIO/Pad/PadBase.h" -PadBase::PadBase(u8 unifiedSlot) +PadBase::PadBase(u8 unifiedSlot, size_t ejectTicks) { this->unifiedSlot = unifiedSlot; + this->ejectTicks = ejectTicks; } PadBase::~PadBase() = default; diff --git a/pcsx2/SIO/Pad/PadBase.h b/pcsx2/SIO/Pad/PadBase.h index c45be5434937b..d485674a69f66 100644 --- a/pcsx2/SIO/Pad/PadBase.h +++ b/pcsx2/SIO/Pad/PadBase.h @@ -23,6 +23,14 @@ class PadBase { +public: + // How many commands the pad should pretend to be ejected for. + // Since changing pads in the UI or ejecting a multitap is instant, + // this simulates the time it would take for a human to plug in the pad. + // That gives games a chance to see a pad get inserted rather than the + // pad's type magically changing from one frame to the next. + size_t ejectTicks = 0; + protected: std::array rawInputs = {}; u8 unifiedSlot; @@ -32,7 +40,7 @@ class PadBase size_t commandBytesReceived = 0; public: // Public members - PadBase(u8 unifiedSlot); + PadBase(u8 unifiedSlot, size_t ejectTicks = 0); virtual ~PadBase(); void SoftReset(); diff --git a/pcsx2/SIO/Pad/PadDualshock2.cpp b/pcsx2/SIO/Pad/PadDualshock2.cpp index d2ef9ca219fa6..9a6e98c291a68 100644 --- a/pcsx2/SIO/Pad/PadDualshock2.cpp +++ b/pcsx2/SIO/Pad/PadDualshock2.cpp @@ -547,8 +547,8 @@ u8 PadDualshock2::ResponseBytes(u8 commandByte) } } -PadDualshock2::PadDualshock2(u8 unifiedSlot) - : PadBase(unifiedSlot) +PadDualshock2::PadDualshock2(u8 unifiedSlot, size_t ejectTicks) + : PadBase(unifiedSlot, ejectTicks) { currentMode = Pad::Mode::DIGITAL; } diff --git a/pcsx2/SIO/Pad/PadDualshock2.h b/pcsx2/SIO/Pad/PadDualshock2.h index b797bc4dc226c..8316792ff3a0e 100644 --- a/pcsx2/SIO/Pad/PadDualshock2.h +++ b/pcsx2/SIO/Pad/PadDualshock2.h @@ -112,7 +112,7 @@ class PadDualshock2 final : public PadBase u8 ResponseBytes(u8 commandByte); public: - PadDualshock2(u8 unifiedSlot); + PadDualshock2(u8 unifiedSlot, size_t ejectTicks); ~PadDualshock2() override; static inline bool IsAnalogKey(int index) diff --git a/pcsx2/SIO/Pad/PadGuitar.cpp b/pcsx2/SIO/Pad/PadGuitar.cpp index 8f092829cd88a..e57c46bf7bc0f 100644 --- a/pcsx2/SIO/Pad/PadGuitar.cpp +++ b/pcsx2/SIO/Pad/PadGuitar.cpp @@ -248,8 +248,8 @@ u8 PadGuitar::VibrationMap(u8 commandByte) return 0xff; } -PadGuitar::PadGuitar(u8 unifiedSlot) - : PadBase(unifiedSlot) +PadGuitar::PadGuitar(u8 unifiedSlot, size_t ejectTicks) + : PadBase(unifiedSlot, ejectTicks) { currentMode = Pad::Mode::DIGITAL; } diff --git a/pcsx2/SIO/Pad/PadGuitar.h b/pcsx2/SIO/Pad/PadGuitar.h index 8b4f28c4ba2a9..886690c4bf3ea 100644 --- a/pcsx2/SIO/Pad/PadGuitar.h +++ b/pcsx2/SIO/Pad/PadGuitar.h @@ -63,7 +63,7 @@ class PadGuitar final : public PadBase u8 VibrationMap(u8 commandByte); public: - PadGuitar(u8 unifiedSlot); + PadGuitar(u8 unifiedSlot, size_t ejectTicks); ~PadGuitar() override; Pad::ControllerType GetType() const override; diff --git a/pcsx2/SIO/Pad/PadNotConnected.cpp b/pcsx2/SIO/Pad/PadNotConnected.cpp index 4e06f84886b1e..b183823fe2025 100644 --- a/pcsx2/SIO/Pad/PadNotConnected.cpp +++ b/pcsx2/SIO/Pad/PadNotConnected.cpp @@ -22,8 +22,8 @@ const Pad::ControllerInfo PadNotConnected::ControllerInfo = {Pad::ControllerType::NotConnected, "None", TRANSLATE_NOOP("Pad", "Not Connected"), {}, {}, Pad::VibrationCapabilities::NoVibration }; -PadNotConnected::PadNotConnected(u8 unifiedSlot) - : PadBase(unifiedSlot) +PadNotConnected::PadNotConnected(u8 unifiedSlot, size_t ejectTicks) + : PadBase(unifiedSlot, ejectTicks) { } diff --git a/pcsx2/SIO/Pad/PadNotConnected.h b/pcsx2/SIO/Pad/PadNotConnected.h index 4183a7d0775f1..10f426a2e2e34 100644 --- a/pcsx2/SIO/Pad/PadNotConnected.h +++ b/pcsx2/SIO/Pad/PadNotConnected.h @@ -20,7 +20,7 @@ class PadNotConnected final : public PadBase { public: - PadNotConnected(u8 unifiedSlot); + PadNotConnected(u8 unifiedSlot, size_t ejectTicks = 0); ~PadNotConnected() override; Pad::ControllerType GetType() const override; diff --git a/pcsx2/SIO/Sio2.cpp b/pcsx2/SIO/Sio2.cpp index dfb0ea29fc764..f99c8e56cbe97 100644 --- a/pcsx2/SIO/Sio2.cpp +++ b/pcsx2/SIO/Sio2.cpp @@ -162,7 +162,7 @@ void Sio2::Pad() this->recv1 |= Recv1::NO_DEVICES_MISSING; // If the currently accessed pad is missing, also tick those bits - if (pad->GetType() == Pad::ControllerType::NotConnected) + if (pad->GetType() == Pad::ControllerType::NotConnected || pad->ejectTicks) { if (!port) { @@ -180,10 +180,28 @@ void Sio2::Pad() // Then for every byte in g_Sio2FifoIn, pass to PAD and see what it kicks back to us. while (!g_Sio2FifoIn.empty()) { - const u8 commandByte = g_Sio2FifoIn.front(); - g_Sio2FifoIn.pop_front(); - const u8 responseByte = pad->SendCommandByte(commandByte); - g_Sio2FifoOut.push_back(responseByte); + // If the pad is "ejected", respond with nothing + if (pad->ejectTicks) + { + g_Sio2FifoIn.pop_front(); + g_Sio2FifoOut.push_back(0xff); + } + // Else, actually forward to the pad. + else + { + const u8 commandByte = g_Sio2FifoIn.front(); + g_Sio2FifoIn.pop_front(); + const u8 responseByte = pad->SendCommandByte(commandByte); + g_Sio2FifoOut.push_back(responseByte); + } + } + + // If the pad is "ejected", then decrement one tick. + // This needs to happen AFTER anything else which might + // consider if the pad is "ejected"! + if (pad->ejectTicks) + { + pad->ejectTicks -= 1; } }