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],