Core: Add Multitap support

This commit is contained in:
Albert Liu 2021-01-20 23:59:40 -08:00
parent bd9cb67565
commit f9dc1a7e80
19 changed files with 590 additions and 51 deletions

View File

@ -67,6 +67,8 @@ add_library(core
memory_card.h memory_card.h
memory_card_image.cpp memory_card_image.cpp
memory_card_image.h memory_card_image.h
multitap.cpp
multitap.h
namco_guncon.cpp namco_guncon.cpp
namco_guncon.h namco_guncon.h
negcon.cpp negcon.cpp

View File

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

View File

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

View File

@ -580,7 +580,12 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si)
si.SetBoolValue("BIOS", "PatchFastBoot", false); si.SetBoolValue("BIOS", "PatchFastBoot", false);
si.SetStringValue("Controller1", "Type", Settings::GetControllerTypeName(Settings::DEFAULT_CONTROLLER_1_TYPE)); 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", "Card1Type", Settings::GetMemoryCardTypeName(Settings::DEFAULT_MEMORY_CARD_1_TYPE));
si.SetStringValue("MemoryCards", "Card1Path", "memcards" FS_OSPATH_SEPARATOR_STR "shared_card_1.mcd"); 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.SetStringValue("MemoryCards", "Card2Path", "memcards" FS_OSPATH_SEPARATOR_STR "shared_card_2.mcd");
si.SetBoolValue("MemoryCards", "UsePlaylistTitle", true); 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", "LogLevel", Settings::GetLogLevelName(Settings::DEFAULT_LOG_LEVEL));
si.SetStringValue("Logging", "LogFilter", ""); si.SetStringValue("Logging", "LogFilter", "");
si.SetBoolValue("Logging", "LogToConsole", Settings::DEFAULT_LOG_TO_CONSOLE); 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) if (m_display && g_settings.display_linear_filtering != old_settings.display_linear_filtering)
m_display->SetDisplayLinearFiltering(g_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 "host_interface.h"
#include "interrupt_controller.h" #include "interrupt_controller.h"
#include "memory_card.h" #include "memory_card.h"
#include "multitap.h"
#include "system.h" #include "system.h"
Log_SetChannel(Pad); Log_SetChannel(Pad);
@ -27,7 +28,7 @@ void Pad::Shutdown()
{ {
m_transfer_event.reset(); 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_controllers[i].reset();
m_memory_cards[i].reset(); m_memory_cards[i].reset();
@ -38,7 +39,7 @@ void Pad::Reset()
{ {
SoftReset(); SoftReset();
for (u32 i = 0; i < NUM_SLOTS; i++) for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
{ {
if (m_controllers[i]) if (m_controllers[i])
m_controllers[i]->Reset(); m_controllers[i]->Reset();
@ -46,12 +47,18 @@ void Pad::Reset()
if (m_memory_cards[i]) if (m_memory_cards[i])
m_memory_cards[i]->Reset(); m_memory_cards[i]->Reset();
} }
for (u32 i = 0; i < NUM_MULTITAPS; i++)
m_multitaps[i].Reset();
} }
bool Pad::DoState(StateWrapper& sw) 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 controller_type = m_controllers[i] ? m_controllers[i]->GetType() : ControllerType::None;
ControllerType state_controller_type = controller_type; ControllerType state_controller_type = controller_type;
sw.Do(&state_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_state);
sw.Do(&m_JOY_CTRL.bits); sw.Do(&m_JOY_CTRL.bits);
sw.Do(&m_JOY_STAT.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); 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) u32 Pad::ReadRegister(u32 offset)
{ {
switch (offset) switch (offset)
@ -409,8 +434,9 @@ void Pad::DoTransfer(TickCount ticks_late)
{ {
Log_DebugPrintf("Transferring slot %d", m_JOY_CTRL.SLOT.GetValue()); Log_DebugPrintf("Transferring slot %d", m_JOY_CTRL.SLOT.GetValue());
Controller* const controller = m_controllers[m_JOY_CTRL.SLOT].get(); const u8 device_index = m_multitaps[0].IsEnabled() ? 4u : m_JOY_CTRL.SLOT;
MemoryCard* const memory_card = m_memory_cards[m_JOY_CTRL.SLOT].get(); Controller* const controller = m_controllers[device_index].get();
MemoryCard* const memory_card = m_memory_cards[device_index].get();
// set rx? // set rx?
m_JOY_CTRL.RXEN = true; m_JOY_CTRL.RXEN = true;
@ -424,25 +450,37 @@ void Pad::DoTransfer(TickCount ticks_late)
{ {
case ActiveDevice::None: 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("Active device set to tap %d, sent 0x%02X, received 0x%02X",
Log_TracePrintf("Nothing connected or ACK'ed"); static_cast<int>(m_JOY_CTRL.SLOT), data_out, data_in);
} m_active_device = ActiveDevice::Multitap;
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 else
{ {
// controller responded, make it the active device until non-ack if (!controller || (ack = controller->Transfer(data_out, &data_in)) == false)
Log_TracePrintf("Transfer to controller, data_out=0x%02X, data_in=0x%02X", data_out, data_in); {
m_active_device = ActiveDevice::Controller; 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; break;
@ -466,6 +504,17 @@ void Pad::DoTransfer(TickCount ticks_late)
} }
} }
break; 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; m_receive_buffer = data_in;
@ -479,7 +528,11 @@ void Pad::DoTransfer(TickCount ticks_late)
} }
else 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); Log_DebugPrintf("Delaying ACK for %d ticks", ack_timer);
m_state = State::WaitingForACK; m_state = State::WaitingForACK;
m_transfer_event->SetPeriodAndSchedule(ack_timer); m_transfer_event->SetPeriodAndSchedule(ack_timer);
@ -517,13 +570,16 @@ void Pad::EndTransfer()
void Pad::ResetDeviceTransferState() 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]) if (m_controllers[i])
m_controllers[i]->ResetTransferState(); m_controllers[i]->ResetTransferState();
if (m_memory_cards[i]) if (m_memory_cards[i])
m_memory_cards[i]->ResetTransferState(); 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 #pragma once
#include "common/bitfield.h" #include "common/bitfield.h"
#include "common/fifo_queue.h" #include "common/fifo_queue.h"
#include "multitap.h"
#include "types.h" #include "types.h"
#include <array> #include <array>
#include <memory> #include <memory>
@ -28,6 +29,9 @@ public:
MemoryCard* GetMemoryCard(u32 slot) { return m_memory_cards[slot].get(); } MemoryCard* GetMemoryCard(u32 slot) { return m_memory_cards[slot].get(); }
void SetMemoryCard(u32 slot, std::unique_ptr<MemoryCard> dev); 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); u32 ReadRegister(u32 offset);
void WriteRegister(u32 offset, u32 value); void WriteRegister(u32 offset, u32 value);
@ -47,7 +51,8 @@ private:
{ {
None, None,
Controller, Controller,
MemoryCard MemoryCard,
Multitap
}; };
union JOY_CTRL union JOY_CTRL
@ -108,8 +113,10 @@ private:
void EndTransfer(); void EndTransfer();
void ResetDeviceTransferState(); void ResetDeviceTransferState();
std::array<std::unique_ptr<Controller>, NUM_SLOTS> m_controllers; std::array<std::unique_ptr<Controller>, NUM_CONTROLLER_AND_CARD_PORTS> m_controllers;
std::array<std::unique_ptr<MemoryCard>, NUM_SLOTS> m_memory_cards; 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; std::unique_ptr<TimingEvent> m_transfer_event;
State m_state = State::Idle; State m_state = State::Idle;

View File

@ -2,7 +2,7 @@
#include "types.h" #include "types.h"
static constexpr u32 SAVE_STATE_MAGIC = 0x43435544; 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 constexpr u32 SAVE_STATE_MINIMUM_VERSION = 42;
static_assert(SAVE_STATE_VERSION >= SAVE_STATE_MINIMUM_VERSION); static_assert(SAVE_STATE_VERSION >= SAVE_STATE_MINIMUM_VERSION);

View File

@ -1,4 +1,5 @@
#include "settings.h" #include "settings.h"
#include "common/assert.h"
#include "common/file_system.h" #include "common/file_system.h"
#include "common/make_array.h" #include "common/make_array.h"
#include "common/string_util.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) void Settings::CPUOverclockPercentToFraction(u32 percent, u32* numerator, u32* denominator)
{ {
const u32 percent_gcd = std::gcd(percent, 100); const u32 percent_gcd = std::gcd(percent, 100);
@ -231,11 +257,15 @@ void Settings::Load(SettingsInterface& si)
ParseControllerTypeName( ParseControllerTypeName(
si.GetStringValue("Controller1", "Type", GetControllerTypeName(DEFAULT_CONTROLLER_1_TYPE)).c_str()) si.GetStringValue("Controller1", "Type", GetControllerTypeName(DEFAULT_CONTROLLER_1_TYPE)).c_str())
.value_or(DEFAULT_CONTROLLER_1_TYPE); .value_or(DEFAULT_CONTROLLER_1_TYPE);
controller_types[1] =
ParseControllerTypeName( for (u32 i = 1; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
si.GetStringValue("Controller2", "Type", GetControllerTypeName(DEFAULT_CONTROLLER_2_TYPE)).c_str()) {
.value_or(DEFAULT_CONTROLLER_2_TYPE); controller_types[i] =
controller_disable_analog_mode_forcing = false; 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] = memory_card_types[0] =
ParseMemoryCardTypeName( ParseMemoryCardTypeName(
@ -251,6 +281,11 @@ void Settings::Load(SettingsInterface& si)
si.GetStringValue("MemoryCards", "Card2Path", "memcards" FS_OSPATH_SEPARATOR_STR "shared_card_2.mcd"); si.GetStringValue("MemoryCards", "Card2Path", "memcards" FS_OSPATH_SEPARATOR_STR "shared_card_2.mcd");
memory_card_use_playlist_title = si.GetBoolValue("MemoryCards", "UsePlaylistTitle", true); 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()) log_level = ParseLogLevelName(si.GetStringValue("Logging", "LogLevel", GetLogLevelName(DEFAULT_LOG_LEVEL)).c_str())
.value_or(DEFAULT_LOG_LEVEL); .value_or(DEFAULT_LOG_LEVEL);
log_filter = si.GetStringValue("Logging", "LogFilter", ""); 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.SetStringValue("MemoryCards", "Card2Path", memory_card_paths[1].c_str());
si.SetBoolValue("MemoryCards", "UsePlaylistTitle", memory_card_use_playlist_title); 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", "LogLevel", GetLogLevelName(log_level));
si.SetStringValue("Logging", "LogFilter", log_filter.c_str()); si.SetStringValue("Logging", "LogFilter", log_filter.c_str());
si.SetBoolValue("Logging", "LogToConsole", log_to_console); 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)]; 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{}; std::array<std::string, NUM_CONTROLLER_AND_CARD_PORTS> memory_card_paths{};
bool memory_card_use_playlist_title = true; bool memory_card_use_playlist_title = true;
MultitapMode multitap_mode = MultitapMode::Disabled;
LOGLEVEL log_level = LOGLEVEL_INFO; LOGLEVEL log_level = LOGLEVEL_INFO;
std::string log_filter; std::string log_filter;
bool log_to_console = false; bool log_to_console = false;
@ -251,6 +253,8 @@ struct Settings
bool HasAnyPerGameMemoryCards() const; bool HasAnyPerGameMemoryCards() const;
bool IsMultitapEnabledOnPort(u32 port) const;
static void CPUOverclockPercentToFraction(u32 percent, u32* numerator, u32* denominator); static void CPUOverclockPercentToFraction(u32 percent, u32* numerator, u32* denominator);
static u32 CPUOverclockFractionToPercent(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* GetMemoryCardTypeName(MemoryCardType type);
static const char* GetMemoryCardTypeDisplayName(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. // Default to D3D11 on Windows as it's more performant and at this point, less buggy.
#ifdef WIN32 #ifdef WIN32
static constexpr GPURenderer DEFAULT_GPU_RENDERER = GPURenderer::HardwareD3D11; 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 ControllerType DEFAULT_CONTROLLER_2_TYPE = ControllerType::None;
static constexpr MemoryCardType DEFAULT_MEMORY_CARD_1_TYPE = MemoryCardType::PerGameTitle; static constexpr MemoryCardType DEFAULT_MEMORY_CARD_1_TYPE = MemoryCardType::PerGameTitle;
static constexpr MemoryCardType DEFAULT_MEMORY_CARD_2_TYPE = MemoryCardType::None; 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; static constexpr LOGLEVEL DEFAULT_LOG_LEVEL = LOGLEVEL_INFO;
// Enable console logging by default on Linux platforms. // Enable console logging by default on Linux platforms.

View File

@ -24,6 +24,7 @@
#include "libcrypt_game_codes.h" #include "libcrypt_game_codes.h"
#include "mdec.h" #include "mdec.h"
#include "memory_card.h" #include "memory_card.h"
#include "multitap.h"
#include "pad.h" #include "pad.h"
#include "psf_loader.h" #include "psf_loader.h"
#include "save_state_version.h" #include "save_state_version.h"
@ -828,6 +829,7 @@ bool Boot(const SystemBootParameters& params)
Bus::SetBIOS(*bios_image); Bus::SetBIOS(*bios_image);
UpdateControllers(); UpdateControllers();
UpdateMemoryCards(); UpdateMemoryCards();
UpdateMultitaps();
Reset(); Reset();
// Enable tty by patching bios. // Enable tty by patching bios.
@ -1223,6 +1225,7 @@ bool DoLoadState(ByteStream* state, bool force_software_renderer, bool update_di
UpdateControllers(); UpdateControllers();
UpdateMemoryCards(); UpdateMemoryCards();
UpdateMultitaps();
} }
else 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) bool DumpRAM(const char* filename)
{ {
if (!IsValid()) if (!IsValid())

View File

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

View File

@ -143,9 +143,18 @@ enum class MemoryCardType
Count Count
}; };
enum class MultitapMode
{
Disabled,
Port1Only,
BothPorts,
Count
};
enum : u32 enum : u32
{ {
NUM_CONTROLLER_AND_CARD_PORTS = 2 NUM_CONTROLLER_AND_CARD_PORTS = 8,
NUM_MULTITAPS = 2
}; };
enum class CPUFastmemMode enum class CPUFastmemMode

View File

@ -22,6 +22,12 @@ ConsoleSettingsWidget::ConsoleSettingsWidget(QtHostInterface* host_interface, QW
qApp->translate("CPUExecutionMode", Settings::GetCPUExecutionModeDisplayName(static_cast<CPUExecutionMode>(i)))); 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", SettingWidgetBinder::BindWidgetToEnumSetting(m_host_interface, m_ui.region, "Console", "Region",
&Settings::ParseConsoleRegionName, &Settings::GetConsoleRegionName, &Settings::ParseConsoleRegionName, &Settings::GetConsoleRegionName,
Settings::DEFAULT_CONSOLE_REGION); 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.cdromRegionCheck, "CDROM", "RegionCheck");
SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.cdromLoadImageToRAM, "CDROM", "LoadImageToRAM", SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.cdromLoadImageToRAM, "CDROM", "LoadImageToRAM",
false); false);
SettingWidgetBinder::BindWidgetToEnumSetting(m_host_interface, m_ui.multitapMode, "ControllerPorts", "MultitapMode",
&Settings::ParseMultitapModeName, &Settings::GetMultitapModeName,
Settings::DEFAULT_MULTITAP_MODE);
dialog->registerWidgetHelp( dialog->registerWidgetHelp(m_ui.region, tr("Region"), tr("Auto-Detect"),
m_ui.region, tr("Region"), tr("Auto-Detect"), tr("Determines the emulated hardware type."));
tr("Determines the emulated hardware type.")); dialog->registerWidgetHelp(m_ui.cpuExecutionMode, tr("Execution Mode"), tr("Recompiler (Fastest)"),
dialog->registerWidgetHelp( tr("Determines how the emulated CPU executes instructions."));
m_ui.cpuExecutionMode, tr("Execution Mode"), tr("Recompiler (Fastest)"), dialog->registerWidgetHelp(m_ui.enableCPUClockSpeedControl,
tr("Determines how the emulated CPU executes instructions.")); tr("Enable Clock Speed Control (Overclocking/Underclocking)"), tr("Unchecked"),
dialog->registerWidgetHelp( tr("When this option is chosen, the clock speed set below will be used."));
m_ui.enableCPUClockSpeedControl, tr("Enable Clock Speed Control (Overclocking/Underclocking)"), tr("Unchecked"), dialog->registerWidgetHelp(m_ui.cpuClockSpeed, tr("Overclocking Percentage"), tr("100%"),
tr("When this option is chosen, the clock speed set below will be used.")); tr("Selects the percentage of the normal clock speed the emulated hardware will run at."));
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( dialog->registerWidgetHelp(
m_ui.cdromReadSpeedup, tr("CDROM Read Speedup"), tr("None (Double Speed)"), 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 " 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( dialog->registerWidgetHelp(
m_ui.cdromReadThread, tr("Use Read Thread (Asynchronous)"), tr("Checked"), 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.")); tr("Reduces hitches in emulation by reading/decompressing CD data asynchronously on a worker thread."));
dialog->registerWidgetHelp( dialog->registerWidgetHelp(m_ui.cdromRegionCheck, tr("Enable Region Check"), tr("Checked"),
m_ui.cdromRegionCheck, tr("Enable Region Check"), tr("Checked"), tr("Simulates the region check present in original, unmodified consoles."));
tr("Simulates the region check present in original, unmodified consoles."));
dialog->registerWidgetHelp( dialog->registerWidgetHelp(
m_ui.cdromLoadImageToRAM, tr("Preload Image to RAM"), tr("Unchecked"), 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 " 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.")); "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.cpuClockSpeed->setEnabled(m_ui.enableCPUClockSpeedControl->checkState() == Qt::Checked);
m_ui.cdromReadSpeedup->setCurrentIndex(m_host_interface->GetIntSettingValue("CDROM", "ReadSpeedup", 1) - 1); m_ui.cdromReadSpeedup->setCurrentIndex(m_host_interface->GetIntSettingValue("CDROM", "ReadSpeedup", 1) - 1);

View File

@ -219,6 +219,25 @@
</layout> </layout>
</widget> </widget>
</item> </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> <item>
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">

View File

@ -50,5 +50,5 @@ private:
void onLoadProfileClicked(); void onLoadProfileClicked();
void onSaveProfileClicked(); 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(); StopControllerRumble();
m_controller_vibration_motors.clear(); 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]; const ControllerType ctype = g_settings.controller_types[controller_index];
if (ctype == ControllerType::None) 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.", "Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay.",
&s_settings_copy.cdrom_load_image_to_ram); &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(); EndMenuButtons();
} }
break; break;
@ -1569,7 +1574,16 @@ void DrawSettingsWindow()
for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++) 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( settings_changed |= EnumChoiceButton(
TinyString::FromFormat(ICON_FA_GAMEPAD " Controller Type##type%u", port), TinyString::FromFormat(ICON_FA_GAMEPAD " Controller Type##type%u", port),
"Determines the simulated controller plugged into this port.", &s_settings_copy.controller_types[port], "Determines the simulated controller plugged into this port.", &s_settings_copy.controller_types[port],