diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a1b5a083a..f9dccc2da 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -67,6 +67,8 @@ add_library(core memory_card.h memory_card_image.cpp memory_card_image.h + multitap.cpp + multitap.h namco_guncon.cpp namco_guncon.h negcon.cpp diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 6142f7c8c..30d622791 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -136,6 +136,7 @@ + @@ -213,6 +214,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 91d6d1d25..03e5d4196 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -57,6 +57,7 @@ + @@ -116,5 +117,6 @@ + \ No newline at end of file diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index 5163cb484..750d3a185 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -580,7 +580,12 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si) si.SetBoolValue("BIOS", "PatchFastBoot", false); si.SetStringValue("Controller1", "Type", Settings::GetControllerTypeName(Settings::DEFAULT_CONTROLLER_1_TYPE)); - si.SetStringValue("Controller2", "Type", Settings::GetControllerTypeName(Settings::DEFAULT_CONTROLLER_2_TYPE)); + + for (u32 i = 1; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) + { + si.SetStringValue(TinyString::FromFormat("Controller%u", i + 1u), "Type", + Settings::GetControllerTypeName(Settings::DEFAULT_CONTROLLER_2_TYPE)); + } si.SetStringValue("MemoryCards", "Card1Type", Settings::GetMemoryCardTypeName(Settings::DEFAULT_MEMORY_CARD_1_TYPE)); si.SetStringValue("MemoryCards", "Card1Path", "memcards" FS_OSPATH_SEPARATOR_STR "shared_card_1.mcd"); @@ -588,6 +593,9 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si) si.SetStringValue("MemoryCards", "Card2Path", "memcards" FS_OSPATH_SEPARATOR_STR "shared_card_2.mcd"); si.SetBoolValue("MemoryCards", "UsePlaylistTitle", true); + si.SetStringValue("ControllerPorts", "MultitapMode", + Settings::GetMultitapModeName(Settings::DEFAULT_MULTITAP_MODE)); + si.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(Settings::DEFAULT_LOG_LEVEL)); si.SetStringValue("Logging", "LogFilter", ""); si.SetBoolValue("Logging", "LogToConsole", Settings::DEFAULT_LOG_TO_CONSOLE); @@ -873,6 +881,9 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings) } } + if (g_settings.multitap_mode != old_settings.multitap_mode) + System::UpdateMultitaps(); + if (m_display && g_settings.display_linear_filtering != old_settings.display_linear_filtering) m_display->SetDisplayLinearFiltering(g_settings.display_linear_filtering); diff --git a/src/core/multitap.cpp b/src/core/multitap.cpp new file mode 100644 index 000000000..5516e900a --- /dev/null +++ b/src/core/multitap.cpp @@ -0,0 +1,244 @@ +#include "multitap.h" +#include "common/log.h" +#include "common/state_wrapper.h" +#include "common/types.h" +#include "controller.h" +#include "memory_card.h" +#include "pad.h" +Log_SetChannel(Multitap); + +Multitap::Multitap(u32 index) : m_index(index) +{ + m_index = index; + Reset(); +} + +void Multitap::Reset() +{ + m_transfer_state = TransferState::Idle; + m_selected_slot = 0; + m_controller_transfer_step = 0; + m_transfer_all_controllers = false; + m_invalid_transfer_all_command = false; + m_current_controller_done = false; + m_transfer_buffer.fill(0xFF); +} + +bool Multitap::DoState(StateWrapper& sw) +{ + sw.Do(&m_transfer_state); + sw.Do(&m_selected_slot); + sw.Do(&m_controller_transfer_step); + sw.Do(&m_invalid_transfer_all_command); + sw.Do(&m_transfer_all_controllers); + sw.Do(&m_current_controller_done); + sw.Do(&m_transfer_buffer); + + return !sw.HasError(); +} + +void Multitap::ResetTransferState() +{ + m_transfer_state = TransferState::Idle; + m_selected_slot = 0; + m_controller_transfer_step = 0; + m_current_controller_done = false; + + // Don't reset m_transfer_all_controllers here, since it's queued up for the next transfer sequence + // Controller and memory card transfer resets are handled in the Pad class +} + +Controller* Multitap::GetControllerForSlot(u32 slot) const +{ + return g_pad.GetController(m_index * 4 + slot); +} + +MemoryCard* Multitap::GetMemoryCardForSlot(u32 slot) const +{ + return g_pad.GetMemoryCard(m_index * 4 + slot); +} + +bool Multitap::TransferController(u32 slot, const u8 data_in, u8* data_out) const +{ + Controller* const selected_controller = GetControllerForSlot(slot); + if (!selected_controller) + { + *data_out = 0xFF; + return false; + } + + return selected_controller->Transfer(data_in, data_out); +} + +bool Multitap::TransferMemoryCard(u32 slot, const u8 data_in, u8* data_out) const +{ + MemoryCard* const selected_memcard = GetMemoryCardForSlot(slot); + if (!selected_memcard) + { + *data_out = 0xFF; + return false; + } + + return selected_memcard->Transfer(data_in, data_out); +} + +bool Multitap::Transfer(const u8 data_in, u8* data_out) +{ + bool ack = false; + switch (m_transfer_state) + { + case TransferState::Idle: + { + switch (data_in) + { + case 0x81: + case 0x82: + case 0x83: + case 0x84: + { + m_selected_slot = (data_in & 0x0F) - 1u; + ack = TransferMemoryCard(m_selected_slot, 0x81, data_out); + + if (ack) + m_transfer_state = TransferState::MemoryCard; + } + break; + + case 0x01: + case 0x02: + case 0x03: + case 0x04: + { + m_selected_slot = data_in - 1u; + ack = TransferController(m_selected_slot, 0x01, data_out); + + if (ack) + { + m_transfer_state = TransferState::ControllerCommand; + + if (m_transfer_all_controllers) + { + // Send access byte to remaining controllers for this transfer mode + u8 dummy_value; + for (u32 i = 0; i < 4; i++) + { + if (i != m_selected_slot) + TransferController(i, 0x01, &dummy_value); + } + } + } + } + break; + + default: + { + *data_out = 0xFF; + ack = false; + } + break; + } + } + break; + + case TransferState::MemoryCard: + { + ack = TransferMemoryCard(m_selected_slot, data_in, data_out); + + if (!ack) + { + Log_DevPrintf("Memory card transfer ended"); + m_transfer_state = TransferState::Idle; + } + } + break; + + case TransferState::ControllerCommand: + { + if (m_controller_transfer_step == 0) // Command byte + { + if (m_transfer_all_controllers) + { + // Unknown if 0x42 is the only valid command byte here, but other tested command bytes cause early aborts + *data_out = GetMultitapIDByte(); + m_invalid_transfer_all_command = (data_in != 0x42); + ack = true; + } + else + { + ack = TransferController(m_selected_slot, data_in, data_out); + } + m_controller_transfer_step++; + } + else if (m_controller_transfer_step == 1) // Request byte + { + if (m_transfer_all_controllers) + { + *data_out = GetStatusByte(); + + ack = !m_invalid_transfer_all_command; + m_selected_slot = 0; + m_transfer_state = TransferState::AllControllers; + } + else + { + ack = TransferController(m_selected_slot, 0x00, data_out); + m_transfer_state = TransferState::SingleController; + } + + // Queue up request for next transfer cycle (not sure if this is always queued on invalid commands) + m_transfer_all_controllers = (data_in & 0x01); + m_controller_transfer_step = 0; + } + else + { + UnreachableCode(); + } + } + break; + + case TransferState::SingleController: + { + // TODO: Check if the transfer buffer get wiped when transitioning to/from this mode + + ack = TransferController(m_selected_slot, data_in, data_out); + + if (!ack) + { + Log_DevPrintf("Controller transfer ended"); + m_transfer_state = TransferState::Idle; + } + } + break; + + case TransferState::AllControllers: + { + // In this mode, we transfer until reaching 8 bytes or the controller finishes its response (no ack is returned). + // The hardware is probably either latching the controller info halfword count or waiting for a transfer timeout + // (timeouts might be possible due to buffered responses in this mode, and if the controllers are transferred in + // parallel rather than sequentially like we're doing here). We'll just simplify this and check the ack return + // value since our controller implementations are deterministic. + + *data_out = m_transfer_buffer[m_controller_transfer_step]; + ack = true; + + if (m_current_controller_done) + m_transfer_buffer[m_controller_transfer_step] = 0xFF; + else + m_current_controller_done = + !TransferController(m_selected_slot, data_in, &m_transfer_buffer[m_controller_transfer_step]); + + m_controller_transfer_step++; + if (m_controller_transfer_step % 8 == 0) + { + m_current_controller_done = false; + m_selected_slot = (m_selected_slot + 1) % 4; + if (m_selected_slot == 0) + ack = false; + } + } + break; + + DefaultCaseIsUnreachable(); + } + return ack; +} diff --git a/src/core/multitap.h b/src/core/multitap.h new file mode 100644 index 000000000..d77de809d --- /dev/null +++ b/src/core/multitap.h @@ -0,0 +1,56 @@ +#pragma once +#include "common/state_wrapper.h" +#include "common/types.h" +#include "controller.h" +#include "memory_card.h" +#include + +class Multitap final +{ +public: + Multitap(u32 index); + + void Reset(); + + ALWAYS_INLINE void SetEnable(bool enable) { m_enabled = enable; }; + ALWAYS_INLINE bool IsEnabled() const { return m_enabled; }; + + bool DoState(StateWrapper& sw); + + void ResetTransferState(); + bool Transfer(const u8 data_in, u8* data_out); + ALWAYS_INLINE bool IsReadingMemoryCard() { return m_enabled && m_transfer_state == TransferState::MemoryCard; }; + +private: + ALWAYS_INLINE static constexpr u8 GetMultitapIDByte() { return 0x80; }; + ALWAYS_INLINE static constexpr u8 GetStatusByte() { return 0x5A; }; + + Controller* GetControllerForSlot(u32 slot) const; + MemoryCard* GetMemoryCardForSlot(u32 slot) const; + + bool TransferController(u32 slot, const u8 data_in, u8* data_out) const; + bool TransferMemoryCard(u32 slot, const u8 data_in, u8* data_out) const; + + enum class TransferState : u8 + { + Idle, + MemoryCard, + ControllerCommand, + SingleController, + AllControllers + }; + + TransferState m_transfer_state = TransferState::Idle; + u8 m_selected_slot = 0; + + u32 m_controller_transfer_step = 0; + + bool m_invalid_transfer_all_command = false; + bool m_transfer_all_controllers = false; + bool m_current_controller_done = false; + + std::array m_transfer_buffer{}; + + u32 m_index; + bool m_enabled = false; +}; diff --git a/src/core/pad.cpp b/src/core/pad.cpp index 1c1cc1c61..044a8d909 100644 --- a/src/core/pad.cpp +++ b/src/core/pad.cpp @@ -5,6 +5,7 @@ #include "host_interface.h" #include "interrupt_controller.h" #include "memory_card.h" +#include "multitap.h" #include "system.h" Log_SetChannel(Pad); @@ -27,7 +28,7 @@ void Pad::Shutdown() { m_transfer_event.reset(); - for (u32 i = 0; i < NUM_SLOTS; i++) + for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { m_controllers[i].reset(); m_memory_cards[i].reset(); @@ -38,7 +39,7 @@ void Pad::Reset() { SoftReset(); - for (u32 i = 0; i < NUM_SLOTS; i++) + for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { if (m_controllers[i]) m_controllers[i]->Reset(); @@ -46,12 +47,18 @@ void Pad::Reset() if (m_memory_cards[i]) m_memory_cards[i]->Reset(); } + + for (u32 i = 0; i < NUM_MULTITAPS; i++) + m_multitaps[i].Reset(); } bool Pad::DoState(StateWrapper& sw) { - for (u32 i = 0; i < NUM_SLOTS; i++) + for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { + if (i > 1 && sw.GetVersion() < 50) + continue; + ControllerType controller_type = m_controllers[i] ? m_controllers[i]->GetType() : ControllerType::None; ControllerType state_controller_type = controller_type; sw.Do(&state_controller_type); @@ -205,6 +212,15 @@ bool Pad::DoState(StateWrapper& sw) } } + if (sw.GetVersion() > 49) + { + for (u32 i = 0; i < NUM_MULTITAPS; i++) + { + if (!m_multitaps[i].DoState(sw)) + return false; + } + } + sw.Do(&m_state); sw.Do(&m_JOY_CTRL.bits); sw.Do(&m_JOY_STAT.bits); @@ -231,6 +247,15 @@ void Pad::SetMemoryCard(u32 slot, std::unique_ptr dev) m_memory_cards[slot] = std::move(dev); } +void Pad::SetMultitapEnable(u32 port, bool enable) +{ + if (m_multitaps[port].IsEnabled() != enable) + { + m_multitaps[port].SetEnable(enable); + m_multitaps[port].Reset(); + } +} + u32 Pad::ReadRegister(u32 offset) { switch (offset) @@ -409,8 +434,9 @@ void Pad::DoTransfer(TickCount ticks_late) { Log_DebugPrintf("Transferring slot %d", m_JOY_CTRL.SLOT.GetValue()); - Controller* const controller = m_controllers[m_JOY_CTRL.SLOT].get(); - MemoryCard* const memory_card = m_memory_cards[m_JOY_CTRL.SLOT].get(); + const u8 device_index = m_multitaps[0].IsEnabled() ? 4u : m_JOY_CTRL.SLOT; + Controller* const controller = m_controllers[device_index].get(); + MemoryCard* const memory_card = m_memory_cards[device_index].get(); // set rx? m_JOY_CTRL.RXEN = true; @@ -424,25 +450,37 @@ void Pad::DoTransfer(TickCount ticks_late) { case ActiveDevice::None: { - if (!controller || (ack = controller->Transfer(data_out, &data_in)) == false) + if (m_multitaps[m_JOY_CTRL.SLOT].IsEnabled()) { - if (!memory_card || (ack = memory_card->Transfer(data_out, &data_in)) == false) + if ((ack = m_multitaps[m_JOY_CTRL.SLOT].Transfer(data_out, &data_in)) == true) { - // nothing connected to this port - Log_TracePrintf("Nothing connected or ACK'ed"); - } - else - { - // memory card responded, make it the active device until non-ack - Log_TracePrintf("Transfer to memory card, data_out=0x%02X, data_in=0x%02X", data_out, data_in); - m_active_device = ActiveDevice::MemoryCard; + Log_TracePrintf("Active device set to tap %d, sent 0x%02X, received 0x%02X", + static_cast(m_JOY_CTRL.SLOT), data_out, data_in); + m_active_device = ActiveDevice::Multitap; } } else { - // controller responded, make it the active device until non-ack - Log_TracePrintf("Transfer to controller, data_out=0x%02X, data_in=0x%02X", data_out, data_in); - m_active_device = ActiveDevice::Controller; + if (!controller || (ack = controller->Transfer(data_out, &data_in)) == false) + { + if (!memory_card || (ack = memory_card->Transfer(data_out, &data_in)) == false) + { + // nothing connected to this port + Log_TracePrintf("Nothing connected or ACK'ed"); + } + else + { + // memory card responded, make it the active device until non-ack + Log_TracePrintf("Transfer to memory card, data_out=0x%02X, data_in=0x%02X", data_out, data_in); + m_active_device = ActiveDevice::MemoryCard; + } + } + else + { + // controller responded, make it the active device until non-ack + Log_TracePrintf("Transfer to controller, data_out=0x%02X, data_in=0x%02X", data_out, data_in); + m_active_device = ActiveDevice::Controller; + } } } break; @@ -466,6 +504,17 @@ void Pad::DoTransfer(TickCount ticks_late) } } break; + + case ActiveDevice::Multitap: + { + if (m_multitaps[m_JOY_CTRL.SLOT].IsEnabled()) + { + ack = m_multitaps[m_JOY_CTRL.SLOT].Transfer(data_out, &data_in); + Log_TracePrintf("Transfer tap %d, sent 0x%02X, received 0x%02X, acked: %s", static_cast(m_JOY_CTRL.SLOT), + data_out, data_in, ack ? "true" : "false"); + } + } + break; } m_receive_buffer = data_in; @@ -479,7 +528,11 @@ void Pad::DoTransfer(TickCount ticks_late) } else { - const TickCount ack_timer = GetACKTicks(m_active_device == ActiveDevice::MemoryCard); + const bool memcard_transfer = + m_active_device == ActiveDevice::MemoryCard || + (m_active_device == ActiveDevice::Multitap && m_multitaps[m_JOY_CTRL.SLOT].IsReadingMemoryCard()); + + const TickCount ack_timer = GetACKTicks(memcard_transfer); Log_DebugPrintf("Delaying ACK for %d ticks", ack_timer); m_state = State::WaitingForACK; m_transfer_event->SetPeriodAndSchedule(ack_timer); @@ -517,13 +570,16 @@ void Pad::EndTransfer() void Pad::ResetDeviceTransferState() { - for (u32 i = 0; i < NUM_SLOTS; i++) + for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { if (m_controllers[i]) m_controllers[i]->ResetTransferState(); if (m_memory_cards[i]) m_memory_cards[i]->ResetTransferState(); - - m_active_device = ActiveDevice::None; } + + for (u32 i = 0; i < NUM_MULTITAPS; i++) + m_multitaps[i].ResetTransferState(); + + m_active_device = ActiveDevice::None; } diff --git a/src/core/pad.h b/src/core/pad.h index f8debd2c1..d25130e9f 100644 --- a/src/core/pad.h +++ b/src/core/pad.h @@ -1,6 +1,7 @@ #pragma once #include "common/bitfield.h" #include "common/fifo_queue.h" +#include "multitap.h" #include "types.h" #include #include @@ -28,6 +29,9 @@ public: MemoryCard* GetMemoryCard(u32 slot) { return m_memory_cards[slot].get(); } void SetMemoryCard(u32 slot, std::unique_ptr dev); + Multitap* GetMultitap(u32 slot) { return &m_multitaps[slot]; }; + void SetMultitapEnable(u32 port, bool enable); + u32 ReadRegister(u32 offset); void WriteRegister(u32 offset, u32 value); @@ -47,7 +51,8 @@ private: { None, Controller, - MemoryCard + MemoryCard, + Multitap }; union JOY_CTRL @@ -108,8 +113,10 @@ private: void EndTransfer(); void ResetDeviceTransferState(); - std::array, NUM_SLOTS> m_controllers; - std::array, NUM_SLOTS> m_memory_cards; + std::array, NUM_CONTROLLER_AND_CARD_PORTS> m_controllers; + std::array, NUM_CONTROLLER_AND_CARD_PORTS> m_memory_cards; + + std::array m_multitaps = {Multitap(0), Multitap(1)}; std::unique_ptr m_transfer_event; State m_state = State::Idle; diff --git a/src/core/save_state_version.h b/src/core/save_state_version.h index 5d99a68be..ac8420c71 100644 --- a/src/core/save_state_version.h +++ b/src/core/save_state_version.h @@ -2,7 +2,7 @@ #include "types.h" static constexpr u32 SAVE_STATE_MAGIC = 0x43435544; -static constexpr u32 SAVE_STATE_VERSION = 49; +static constexpr u32 SAVE_STATE_VERSION = 50; static constexpr u32 SAVE_STATE_MINIMUM_VERSION = 42; static_assert(SAVE_STATE_VERSION >= SAVE_STATE_MINIMUM_VERSION); diff --git a/src/core/settings.cpp b/src/core/settings.cpp index a79b347f6..743574e18 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -1,4 +1,5 @@ #include "settings.h" +#include "common/assert.h" #include "common/file_system.h" #include "common/make_array.h" #include "common/string_util.h" @@ -77,6 +78,31 @@ bool Settings::HasAnyPerGameMemoryCards() const }); } +bool Settings::IsMultitapEnabledOnPort(u32 port) const +{ + if (port < NUM_MULTITAPS) + { + switch (multitap_mode) + { + case MultitapMode::Disabled: + return false; + break; + + case MultitapMode::Port1Only: + return port == 0u; + break; + + case MultitapMode::BothPorts: + return true; + break; + + DefaultCaseIsUnreachable(); + } + } + + return false; +} + void Settings::CPUOverclockPercentToFraction(u32 percent, u32* numerator, u32* denominator) { const u32 percent_gcd = std::gcd(percent, 100); @@ -231,11 +257,15 @@ void Settings::Load(SettingsInterface& si) ParseControllerTypeName( si.GetStringValue("Controller1", "Type", GetControllerTypeName(DEFAULT_CONTROLLER_1_TYPE)).c_str()) .value_or(DEFAULT_CONTROLLER_1_TYPE); - controller_types[1] = - ParseControllerTypeName( - si.GetStringValue("Controller2", "Type", GetControllerTypeName(DEFAULT_CONTROLLER_2_TYPE)).c_str()) - .value_or(DEFAULT_CONTROLLER_2_TYPE); - controller_disable_analog_mode_forcing = false; + + for (u32 i = 1; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) + { + controller_types[i] = + ParseControllerTypeName(si.GetStringValue(TinyString::FromFormat("Controller%u", i + 1u), "Type", + GetControllerTypeName(DEFAULT_CONTROLLER_2_TYPE)) + .c_str()) + .value_or(DEFAULT_CONTROLLER_2_TYPE); + } memory_card_types[0] = ParseMemoryCardTypeName( @@ -251,6 +281,11 @@ void Settings::Load(SettingsInterface& si) si.GetStringValue("MemoryCards", "Card2Path", "memcards" FS_OSPATH_SEPARATOR_STR "shared_card_2.mcd"); memory_card_use_playlist_title = si.GetBoolValue("MemoryCards", "UsePlaylistTitle", true); + multitap_mode = + ParseMultitapModeName( + si.GetStringValue("ControllerPorts", "MultitapMode", GetMultitapModeName(DEFAULT_MULTITAP_MODE)).c_str()) + .value_or(DEFAULT_MULTITAP_MODE); + log_level = ParseLogLevelName(si.GetStringValue("Logging", "LogLevel", GetLogLevelName(DEFAULT_LOG_LEVEL)).c_str()) .value_or(DEFAULT_LOG_LEVEL); log_filter = si.GetStringValue("Logging", "LogFilter", ""); @@ -399,6 +434,8 @@ void Settings::Save(SettingsInterface& si) const si.SetStringValue("MemoryCards", "Card2Path", memory_card_paths[1].c_str()); si.SetBoolValue("MemoryCards", "UsePlaylistTitle", memory_card_use_playlist_title); + si.SetStringValue("ControllerPorts", "MultitapMode", GetMultitapModeName(multitap_mode)); + si.SetStringValue("Logging", "LogLevel", GetLogLevelName(log_level)); si.SetStringValue("Logging", "LogFilter", log_filter.c_str()); si.SetBoolValue("Logging", "LogToConsole", log_to_console); @@ -840,3 +877,32 @@ const char* Settings::GetMemoryCardTypeDisplayName(MemoryCardType type) { return s_memory_card_type_display_names[static_cast(type)]; } + +static std::array s_multitap_enable_mode_names = {{"Disabled", "Port1Only", "BothPorts"}}; +static std::array s_multitap_enable_mode_display_names = { + {TRANSLATABLE("MultitapMode", "Disabled"), TRANSLATABLE("MultitapMode", "Enable on Port 1 only"), + TRANSLATABLE("MultitapMode", "Enable on Ports 1 and 2")}}; + +std::optional Settings::ParseMultitapModeName(const char* str) +{ + u32 index = 0; + for (const char* name : s_multitap_enable_mode_names) + { + if (StringUtil::Strcasecmp(name, str) == 0) + return static_cast(index); + + index++; + } + + return std::nullopt; +} + +const char* Settings::GetMultitapModeName(MultitapMode mode) +{ + return s_multitap_enable_mode_names[static_cast(mode)]; +} + +const char* Settings::GetMultitapModeDisplayName(MultitapMode mode) +{ + return s_multitap_enable_mode_display_names[static_cast(mode)]; +} diff --git a/src/core/settings.h b/src/core/settings.h index 7ca4d1efe..21b2b8562 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -216,6 +216,8 @@ struct Settings std::array memory_card_paths{}; bool memory_card_use_playlist_title = true; + MultitapMode multitap_mode = MultitapMode::Disabled; + LOGLEVEL log_level = LOGLEVEL_INFO; std::string log_filter; bool log_to_console = false; @@ -251,6 +253,8 @@ struct Settings bool HasAnyPerGameMemoryCards() const; + bool IsMultitapEnabledOnPort(u32 port) const; + static void CPUOverclockPercentToFraction(u32 percent, u32* numerator, u32* denominator); static u32 CPUOverclockFractionToPercent(u32 numerator, u32 denominator); @@ -323,6 +327,10 @@ struct Settings static const char* GetMemoryCardTypeName(MemoryCardType type); static const char* GetMemoryCardTypeDisplayName(MemoryCardType type); + static std::optional ParseMultitapModeName(const char* str); + static const char* GetMultitapModeName(MultitapMode mode); + static const char* GetMultitapModeDisplayName(MultitapMode mode); + // Default to D3D11 on Windows as it's more performant and at this point, less buggy. #ifdef WIN32 static constexpr GPURenderer DEFAULT_GPU_RENDERER = GPURenderer::HardwareD3D11; @@ -358,6 +366,8 @@ struct Settings static constexpr ControllerType DEFAULT_CONTROLLER_2_TYPE = ControllerType::None; static constexpr MemoryCardType DEFAULT_MEMORY_CARD_1_TYPE = MemoryCardType::PerGameTitle; static constexpr MemoryCardType DEFAULT_MEMORY_CARD_2_TYPE = MemoryCardType::None; + static constexpr MultitapMode DEFAULT_MULTITAP_MODE = MultitapMode::Disabled; + static constexpr LOGLEVEL DEFAULT_LOG_LEVEL = LOGLEVEL_INFO; // Enable console logging by default on Linux platforms. diff --git a/src/core/system.cpp b/src/core/system.cpp index a0c7538e9..30abfd5ff 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -24,6 +24,7 @@ #include "libcrypt_game_codes.h" #include "mdec.h" #include "memory_card.h" +#include "multitap.h" #include "pad.h" #include "psf_loader.h" #include "save_state_version.h" @@ -828,6 +829,7 @@ bool Boot(const SystemBootParameters& params) Bus::SetBIOS(*bios_image); UpdateControllers(); UpdateMemoryCards(); + UpdateMultitaps(); Reset(); // Enable tty by patching bios. @@ -1223,6 +1225,7 @@ bool DoLoadState(ByteStream* state, bool force_software_renderer, bool update_di UpdateControllers(); UpdateMemoryCards(); + UpdateMultitaps(); } else { @@ -1836,6 +1839,34 @@ void UpdateMemoryCards() } } +void UpdateMultitaps() +{ + switch (g_settings.multitap_mode) + { + case MultitapMode::Disabled: + { + g_pad.SetMultitapEnable(0, false); + g_pad.SetMultitapEnable(1, false); + } + break; + + case MultitapMode::Port1Only: + { + g_pad.SetMultitapEnable(0, true); + g_pad.SetMultitapEnable(1, false); + } + break; + + case MultitapMode::BothPorts: + { + g_pad.SetMultitapEnable(0, true); + g_pad.SetMultitapEnable(1, true); + } + break; + } +} + + bool DumpRAM(const char* filename) { if (!IsValid()) diff --git a/src/core/system.h b/src/core/system.h index 9fd140bec..4829f9b6f 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -187,6 +187,7 @@ void UpdateControllers(); void UpdateControllerSettings(); void ResetControllers(); void UpdateMemoryCards(); +void UpdateMultitaps(); /// Dumps RAM to a file. bool DumpRAM(const char* filename); diff --git a/src/core/types.h b/src/core/types.h index e17fbe8cd..8083c54d8 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -143,9 +143,18 @@ enum class MemoryCardType Count }; +enum class MultitapMode +{ + Disabled, + Port1Only, + BothPorts, + Count +}; + enum : u32 { - NUM_CONTROLLER_AND_CARD_PORTS = 2 + NUM_CONTROLLER_AND_CARD_PORTS = 8, + NUM_MULTITAPS = 2 }; enum class CPUFastmemMode diff --git a/src/duckstation-qt/consolesettingswidget.cpp b/src/duckstation-qt/consolesettingswidget.cpp index 219ab041e..7b78b1b6b 100644 --- a/src/duckstation-qt/consolesettingswidget.cpp +++ b/src/duckstation-qt/consolesettingswidget.cpp @@ -22,6 +22,12 @@ ConsoleSettingsWidget::ConsoleSettingsWidget(QtHostInterface* host_interface, QW qApp->translate("CPUExecutionMode", Settings::GetCPUExecutionModeDisplayName(static_cast(i)))); } + for (u32 i = 0; i < static_cast(MultitapMode::Count); i++) + { + m_ui.multitapMode->addItem( + qApp->translate("MultitapMode", Settings::GetMultitapModeDisplayName(static_cast(i)))); + } + SettingWidgetBinder::BindWidgetToEnumSetting(m_host_interface, m_ui.region, "Console", "Region", &Settings::ParseConsoleRegionName, &Settings::GetConsoleRegionName, Settings::DEFAULT_CONSOLE_REGION); @@ -34,19 +40,19 @@ ConsoleSettingsWidget::ConsoleSettingsWidget(QtHostInterface* host_interface, QW SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.cdromRegionCheck, "CDROM", "RegionCheck"); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.cdromLoadImageToRAM, "CDROM", "LoadImageToRAM", false); + SettingWidgetBinder::BindWidgetToEnumSetting(m_host_interface, m_ui.multitapMode, "ControllerPorts", "MultitapMode", + &Settings::ParseMultitapModeName, &Settings::GetMultitapModeName, + Settings::DEFAULT_MULTITAP_MODE); - dialog->registerWidgetHelp( - m_ui.region, tr("Region"), tr("Auto-Detect"), - tr("Determines the emulated hardware type.")); - dialog->registerWidgetHelp( - m_ui.cpuExecutionMode, tr("Execution Mode"), tr("Recompiler (Fastest)"), - tr("Determines how the emulated CPU executes instructions.")); - dialog->registerWidgetHelp( - m_ui.enableCPUClockSpeedControl, tr("Enable Clock Speed Control (Overclocking/Underclocking)"), tr("Unchecked"), - tr("When this option is chosen, the clock speed set below will be used.")); - dialog->registerWidgetHelp( - m_ui.cpuClockSpeed, tr("Overclocking Percentage"), tr("100%"), - tr("Selects the percentage of the normal clock speed the emulated hardware will run at.")); + dialog->registerWidgetHelp(m_ui.region, tr("Region"), tr("Auto-Detect"), + tr("Determines the emulated hardware type.")); + dialog->registerWidgetHelp(m_ui.cpuExecutionMode, tr("Execution Mode"), tr("Recompiler (Fastest)"), + tr("Determines how the emulated CPU executes instructions.")); + dialog->registerWidgetHelp(m_ui.enableCPUClockSpeedControl, + tr("Enable Clock Speed Control (Overclocking/Underclocking)"), tr("Unchecked"), + tr("When this option is chosen, the clock speed set below will be used.")); + dialog->registerWidgetHelp(m_ui.cpuClockSpeed, tr("Overclocking Percentage"), tr("100%"), + tr("Selects the percentage of the normal clock speed the emulated hardware will run at.")); dialog->registerWidgetHelp( m_ui.cdromReadSpeedup, tr("CDROM Read Speedup"), tr("None (Double Speed)"), tr("Speeds up CD-ROM reads by the specified factor. Only applies to double-speed reads, and is ignored when audio " @@ -54,13 +60,16 @@ ConsoleSettingsWidget::ConsoleSettingsWidget(QtHostInterface* host_interface, QW dialog->registerWidgetHelp( m_ui.cdromReadThread, tr("Use Read Thread (Asynchronous)"), tr("Checked"), tr("Reduces hitches in emulation by reading/decompressing CD data asynchronously on a worker thread.")); - dialog->registerWidgetHelp( - m_ui.cdromRegionCheck, tr("Enable Region Check"), tr("Checked"), - tr("Simulates the region check present in original, unmodified consoles.")); + dialog->registerWidgetHelp(m_ui.cdromRegionCheck, tr("Enable Region Check"), tr("Checked"), + tr("Simulates the region check present in original, unmodified consoles.")); dialog->registerWidgetHelp( m_ui.cdromLoadImageToRAM, tr("Preload Image to RAM"), tr("Unchecked"), tr("Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay. In some " "cases also eliminates stutter when games initiate audio track playback.")); + dialog->registerWidgetHelp( + m_ui.multitapMode, tr("Multitap"), tr("Disabled"), + tr("Enables multitap support on specified controller ports. Leave disabled for games that do " + "not support multitap input.")); m_ui.cpuClockSpeed->setEnabled(m_ui.enableCPUClockSpeedControl->checkState() == Qt::Checked); m_ui.cdromReadSpeedup->setCurrentIndex(m_host_interface->GetIntSettingValue("CDROM", "ReadSpeedup", 1) - 1); diff --git a/src/duckstation-qt/consolesettingswidget.ui b/src/duckstation-qt/consolesettingswidget.ui index f71e7d805..525027e7e 100644 --- a/src/duckstation-qt/consolesettingswidget.ui +++ b/src/duckstation-qt/consolesettingswidget.ui @@ -219,6 +219,25 @@ + + + + Controller Ports + + + + + + Multitap: + + + + + + + + + diff --git a/src/duckstation-qt/controllersettingswidget.h b/src/duckstation-qt/controllersettingswidget.h index d1b205497..ddb607399 100644 --- a/src/duckstation-qt/controllersettingswidget.h +++ b/src/duckstation-qt/controllersettingswidget.h @@ -50,5 +50,5 @@ private: void onLoadProfileClicked(); void onSaveProfileClicked(); - std::array m_port_ui = {}; + std::array m_port_ui = {}; }; diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 26958e22d..ec1dbb1a5 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -1384,7 +1384,7 @@ void CommonHostInterface::UpdateControllerInputMap(SettingsInterface& si) StopControllerRumble(); m_controller_vibration_motors.clear(); - for (u32 controller_index = 0; controller_index < 2; controller_index++) + for (u32 controller_index = 0; controller_index < NUM_CONTROLLER_AND_CARD_PORTS; controller_index++) { const ControllerType ctype = g_settings.controller_types[controller_index]; if (ctype == ControllerType::None) diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index c7f651a77..ee2603819 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -1315,6 +1315,11 @@ void DrawSettingsWindow() "Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay.", &s_settings_copy.cdrom_load_image_to_ram); + MenuHeading("Controller Ports"); + + settings_changed |= EnumChoiceButton("Multitap", nullptr, &s_settings_copy.multitap_mode, + &Settings::GetMultitapModeDisplayName, MultitapMode::Count); + EndMenuButtons(); } break; @@ -1569,7 +1574,16 @@ void DrawSettingsWindow() for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++) { - MenuHeading(TinyString::FromFormat("Controller Port %u", port + 1)); + u32 console_port = port / 4u; + if (s_settings_copy.IsMultitapEnabledOnPort(console_port)) + MenuHeading(TinyString::FromFormat("Port %u%c", console_port + 1u, 'A' + (port % 4u))); + else if (port < 2u) + MenuHeading(TinyString::FromFormat("Port %u", port + 1u)); + else if (port % 4u == 0u && s_settings_copy.IsMultitapEnabledOnPort(0)) + MenuHeading(TinyString::FromFormat("Port %u", console_port + 1u)); + else + continue; + settings_changed |= EnumChoiceButton( TinyString::FromFormat(ICON_FA_GAMEPAD " Controller Type##type%u", port), "Determines the simulated controller plugged into this port.", &s_settings_copy.controller_types[port],