Merge pull request #1697 from ggrtk/multitap

Core: Add Multitap support
This commit is contained in:
Connor McLaughlin 2021-02-27 16:19:28 +10:00 committed by GitHub
commit 86f44f826d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 590 additions and 51 deletions

View File

@ -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

View File

@ -136,6 +136,7 @@
<ClCompile Include="mdec.cpp" />
<ClCompile Include="memory_card.cpp" />
<ClCompile Include="memory_card_image.cpp" />
<ClCompile Include="multitap.cpp" />
<ClCompile Include="namco_guncon.cpp" />
<ClCompile Include="negcon.cpp" />
<ClCompile Include="pad.cpp" />
@ -213,6 +214,7 @@
<ClInclude Include="mdec.h" />
<ClInclude Include="memory_card.h" />
<ClInclude Include="memory_card_image.h" />
<ClInclude Include="multitap.h" />
<ClInclude Include="namco_guncon.h" />
<ClInclude Include="negcon.h" />
<ClInclude Include="pad.h" />

View File

@ -57,6 +57,7 @@
<ClCompile Include="libcrypt_game_codes.cpp" />
<ClCompile Include="texture_replacements.cpp" />
<ClCompile Include="gdb_protocol.h" />
<ClCompile Include="multitap.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="types.h" />
@ -116,5 +117,6 @@
<ClInclude Include="libcrypt_game_codes.h" />
<ClInclude Include="texture_replacements.h" />
<ClInclude Include="shader_cache_version.h" />
<ClInclude Include="multitap.h" />
</ItemGroup>
</Project>

View File

@ -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);

244
src/core/multitap.cpp Normal file
View File

@ -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;
}

56
src/core/multitap.h Normal file
View File

@ -0,0 +1,56 @@
#pragma once
#include "common/state_wrapper.h"
#include "common/types.h"
#include "controller.h"
#include "memory_card.h"
#include <array>
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<u8, 32> m_transfer_buffer{};
u32 m_index;
bool m_enabled = false;
};

View File

@ -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<MemoryCard> 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<int>(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<int>(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;
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "common/bitfield.h"
#include "common/fifo_queue.h"
#include "multitap.h"
#include "types.h"
#include <array>
#include <memory>
@ -28,6 +29,9 @@ public:
MemoryCard* GetMemoryCard(u32 slot) { return m_memory_cards[slot].get(); }
void SetMemoryCard(u32 slot, std::unique_ptr<MemoryCard> 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<std::unique_ptr<Controller>, NUM_SLOTS> m_controllers;
std::array<std::unique_ptr<MemoryCard>, NUM_SLOTS> m_memory_cards;
std::array<std::unique_ptr<Controller>, NUM_CONTROLLER_AND_CARD_PORTS> m_controllers;
std::array<std::unique_ptr<MemoryCard>, NUM_CONTROLLER_AND_CARD_PORTS> m_memory_cards;
std::array<Multitap, NUM_MULTITAPS> m_multitaps = {Multitap(0), Multitap(1)};
std::unique_ptr<TimingEvent> m_transfer_event;
State m_state = State::Idle;

View File

@ -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);

View File

@ -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<int>(type)];
}
static std::array<const char*, 3> s_multitap_enable_mode_names = {{"Disabled", "Port1Only", "BothPorts"}};
static std::array<const char*, 3> 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<MultitapMode> 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<MultitapMode>(index);
index++;
}
return std::nullopt;
}
const char* Settings::GetMultitapModeName(MultitapMode mode)
{
return s_multitap_enable_mode_names[static_cast<size_t>(mode)];
}
const char* Settings::GetMultitapModeDisplayName(MultitapMode mode)
{
return s_multitap_enable_mode_display_names[static_cast<size_t>(mode)];
}

View File

@ -216,6 +216,8 @@ struct Settings
std::array<std::string, NUM_CONTROLLER_AND_CARD_PORTS> 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<MultitapMode> 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.

View File

@ -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())

View File

@ -187,6 +187,7 @@ void UpdateControllers();
void UpdateControllerSettings();
void ResetControllers();
void UpdateMemoryCards();
void UpdateMultitaps();
/// Dumps RAM to a file.
bool DumpRAM(const char* filename);

View File

@ -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

View File

@ -22,6 +22,12 @@ ConsoleSettingsWidget::ConsoleSettingsWidget(QtHostInterface* host_interface, QW
qApp->translate("CPUExecutionMode", Settings::GetCPUExecutionModeDisplayName(static_cast<CPUExecutionMode>(i))));
}
for (u32 i = 0; i < static_cast<u32>(MultitapMode::Count); i++)
{
m_ui.multitapMode->addItem(
qApp->translate("MultitapMode", Settings::GetMultitapModeDisplayName(static_cast<MultitapMode>(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);

View File

@ -219,6 +219,25 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Controller Ports</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Multitap:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="multitapMode"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">

View File

@ -50,5 +50,5 @@ private:
void onLoadProfileClicked();
void onSaveProfileClicked();
std::array<PortSettingsUI, 2> m_port_ui = {};
std::array<PortSettingsUI, NUM_CONTROLLER_AND_CARD_PORTS> m_port_ui = {};
};

View File

@ -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)

View File

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