Pad: Add controller ejection

This commit is contained in:
RedPanda4552 2023-11-23 23:15:38 -05:00 committed by Connor McLaughlin
parent 2443e06745
commit 86c42a00a3
11 changed files with 100 additions and 37 deletions

View File

@ -25,6 +25,7 @@
#include "IconsFontAwesome5.h" #include "IconsFontAwesome5.h"
#include "VMManager.h"
#include "common/Assertions.h" #include "common/Assertions.h"
#include "common/FileSystem.h" #include "common/FileSystem.h"
#include "common/Path.h" #include "common/Path.h"
@ -57,6 +58,9 @@ namespace Pad
static std::array<std::array<MacroButton, NUM_MACRO_BUTTONS_PER_CONTROLLER>, NUM_CONTROLLER_PORTS> s_macro_buttons; static std::array<std::array<MacroButton, NUM_MACRO_BUTTONS_PER_CONTROLLER>, NUM_CONTROLLER_PORTS> s_macro_buttons;
static std::array<std::unique_ptr<PadBase>, NUM_CONTROLLER_PORTS> s_controllers; static std::array<std::unique_ptr<PadBase>, NUM_CONTROLLER_PORTS> s_controllers;
bool mtapPort0LastState;
bool mtapPort1LastState;
} }
bool Pad::Initialize() bool Pad::Initialize()
@ -90,19 +94,24 @@ void Pad::LoadConfig(const SettingsInterface& si)
{ {
s_macro_buttons = {}; 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++) for (u32 i = 0; i < Pad::NUM_CONTROLLER_PORTS; i++)
{ {
const std::string section = GetConfigSection(i); const std::string section = GetConfigSection(i);
const ControllerInfo* ci = GetControllerInfo(EmuConfig.Pad.Ports[i].Type); const ControllerInfo* ci = GetControllerInfo(EmuConfig.Pad.Ports[i].Type);
pxAssert(ci); 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); 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); pxAssert(pad);
} }
@ -129,6 +138,9 @@ void Pad::LoadConfig(const SettingsInterface& si)
pad->SetAnalogInvertR((invert_r & 1) != 0, (invert_r & 2) != 0); pad->SetAnalogInvertR((invert_r & 1) != 0, (invert_r & 2) != 0);
LoadMacroButtonConfig(si, i, ci, section); LoadMacroButtonConfig(si, i, ci, section);
} }
Pad::mtapPort0LastState = EmuConfig.Pad.MultitapPort0_Enabled;
Pad::mtapPort1LastState = EmuConfig.Pad.MultitapPort1_Enabled;
} }
Pad::ControllerType Pad::GetDefaultPadType(u32 pad) Pad::ControllerType Pad::GetDefaultPadType(u32 pad)
@ -526,35 +538,57 @@ bool Pad::Freeze(StateWrapper& sw)
for (u32 unifiedSlot = 0; unifiedSlot < NUM_CONTROLLER_PORTS; unifiedSlot++) for (u32 unifiedSlot = 0; unifiedSlot < NUM_CONTROLLER_PORTS; unifiedSlot++)
{ {
ControllerType type; PadBase* currentPad = GetPad(unifiedSlot);
sw.Do(&type); ControllerType statePadType;
sw.Do(&statePadType);
if (sw.HasError()) if (sw.HasError())
return false; return false;
PadBase* tempPad; if (!currentPad)
PadBase* pad = GetPad(unifiedSlot);
if (!pad || pad->GetType() != type)
{ {
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); const auto& [port, slot] = sioConvertPadToPortAndSlot(unifiedSlot);
Host::AddIconOSDMessage(fmt::format("UnfreezePad{}Changed", unifiedSlot), ICON_FA_GAMEPAD, Host::AddIconOSDMessage(fmt::format("UnfreezePad{}Changed", unifiedSlot), ICON_FA_GAMEPAD,
fmt::format(TRANSLATE_FS("Pad", fmt::format(TRANSLATE_FS("Pad",
"Controller port {0}, slot {1} has a {2} connected, but the save state has a " "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, port, slot,
GetControllerTypeName(pad ? pad->GetType() : Pad::ControllerType::NotConnected), GetControllerTypeName(currentPad ? currentPad->GetType() : Pad::ControllerType::NotConnected),
GetControllerTypeName(type))); 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);
// Reset the transfer etc state of the pad, at least it has a better chance of surviving. if (currentPad)
if (pad) {
pad->SoftReset(); currentPad->Freeze(sw);
// But we still need to pull the data from the state.. // Now immediately discard whatever malformed pad state we just created, and replace it with a fresh pad loaded
tempPad = CreatePad(unifiedSlot, type); // using whatever the current user settings are. Savestates are, by definition, never going to occur in the middle
pad = tempPad; // 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());
}
} }
// ... else, just run the freeze normally.
if (!pad->Freeze(sw)) else if (currentPad && !currentPad->Freeze(sw))
{
return false; return false;
}
} }
} }
else else

View File

@ -28,6 +28,8 @@ class StateWrapper;
namespace Pad namespace Pad
{ {
constexpr size_t DEFAULT_EJECT_TICKS = 50;
bool Initialize(); bool Initialize();
void Shutdown(); void Shutdown();

View File

@ -17,9 +17,10 @@
#include "SIO/Pad/PadBase.h" #include "SIO/Pad/PadBase.h"
PadBase::PadBase(u8 unifiedSlot) PadBase::PadBase(u8 unifiedSlot, size_t ejectTicks)
{ {
this->unifiedSlot = unifiedSlot; this->unifiedSlot = unifiedSlot;
this->ejectTicks = ejectTicks;
} }
PadBase::~PadBase() = default; PadBase::~PadBase() = default;

View File

@ -23,6 +23,14 @@
class PadBase 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: protected:
std::array<u8, 32> rawInputs = {}; std::array<u8, 32> rawInputs = {};
u8 unifiedSlot; u8 unifiedSlot;
@ -32,7 +40,7 @@ protected:
size_t commandBytesReceived = 0; size_t commandBytesReceived = 0;
public: // Public members public: // Public members
PadBase(u8 unifiedSlot); PadBase(u8 unifiedSlot, size_t ejectTicks = 0);
virtual ~PadBase(); virtual ~PadBase();
void SoftReset(); void SoftReset();

View File

@ -547,8 +547,8 @@ u8 PadDualshock2::ResponseBytes(u8 commandByte)
} }
} }
PadDualshock2::PadDualshock2(u8 unifiedSlot) PadDualshock2::PadDualshock2(u8 unifiedSlot, size_t ejectTicks)
: PadBase(unifiedSlot) : PadBase(unifiedSlot, ejectTicks)
{ {
currentMode = Pad::Mode::DIGITAL; currentMode = Pad::Mode::DIGITAL;
} }

View File

@ -112,7 +112,7 @@ private:
u8 ResponseBytes(u8 commandByte); u8 ResponseBytes(u8 commandByte);
public: public:
PadDualshock2(u8 unifiedSlot); PadDualshock2(u8 unifiedSlot, size_t ejectTicks);
~PadDualshock2() override; ~PadDualshock2() override;
static inline bool IsAnalogKey(int index) static inline bool IsAnalogKey(int index)

View File

@ -248,8 +248,8 @@ u8 PadGuitar::VibrationMap(u8 commandByte)
return 0xff; return 0xff;
} }
PadGuitar::PadGuitar(u8 unifiedSlot) PadGuitar::PadGuitar(u8 unifiedSlot, size_t ejectTicks)
: PadBase(unifiedSlot) : PadBase(unifiedSlot, ejectTicks)
{ {
currentMode = Pad::Mode::DIGITAL; currentMode = Pad::Mode::DIGITAL;
} }

View File

@ -63,7 +63,7 @@ private:
u8 VibrationMap(u8 commandByte); u8 VibrationMap(u8 commandByte);
public: public:
PadGuitar(u8 unifiedSlot); PadGuitar(u8 unifiedSlot, size_t ejectTicks);
~PadGuitar() override; ~PadGuitar() override;
Pad::ControllerType GetType() const override; Pad::ControllerType GetType() const override;

View File

@ -22,8 +22,8 @@
const Pad::ControllerInfo PadNotConnected::ControllerInfo = {Pad::ControllerType::NotConnected, "None", const Pad::ControllerInfo PadNotConnected::ControllerInfo = {Pad::ControllerType::NotConnected, "None",
TRANSLATE_NOOP("Pad", "Not Connected"), {}, {}, Pad::VibrationCapabilities::NoVibration }; TRANSLATE_NOOP("Pad", "Not Connected"), {}, {}, Pad::VibrationCapabilities::NoVibration };
PadNotConnected::PadNotConnected(u8 unifiedSlot) PadNotConnected::PadNotConnected(u8 unifiedSlot, size_t ejectTicks)
: PadBase(unifiedSlot) : PadBase(unifiedSlot, ejectTicks)
{ {
} }

View File

@ -20,7 +20,7 @@
class PadNotConnected final : public PadBase class PadNotConnected final : public PadBase
{ {
public: public:
PadNotConnected(u8 unifiedSlot); PadNotConnected(u8 unifiedSlot, size_t ejectTicks = 0);
~PadNotConnected() override; ~PadNotConnected() override;
Pad::ControllerType GetType() const override; Pad::ControllerType GetType() const override;

View File

@ -162,7 +162,7 @@ void Sio2::Pad()
this->recv1 |= Recv1::NO_DEVICES_MISSING; this->recv1 |= Recv1::NO_DEVICES_MISSING;
// If the currently accessed pad is missing, also tick those bits // 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) 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. // Then for every byte in g_Sio2FifoIn, pass to PAD and see what it kicks back to us.
while (!g_Sio2FifoIn.empty()) while (!g_Sio2FifoIn.empty())
{ {
const u8 commandByte = g_Sio2FifoIn.front(); // If the pad is "ejected", respond with nothing
g_Sio2FifoIn.pop_front(); if (pad->ejectTicks)
const u8 responseByte = pad->SendCommandByte(commandByte); {
g_Sio2FifoOut.push_back(responseByte); 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;
} }
} }