From 5687dd22bd9e7525536462542fb0abee769cd61b Mon Sep 17 00:00:00 2001 From: Stenzek Date: Fri, 13 Dec 2024 23:09:36 +1000 Subject: [PATCH] PIO: Add basic flash cartridge support --- src/common/log_channels.h | 1 + src/core/CMakeLists.txt | 2 + src/core/bus.cpp | 81 +- src/core/bus.h | 6 - src/core/core.vcxproj | 2 + src/core/core.vcxproj.filters | 2 + src/core/cpu_core.cpp | 9 + src/core/pio.cpp | 863 ++++++++++++++++++++++ src/core/pio.h | 48 ++ src/core/save_state_version.h | 2 +- src/core/settings.cpp | 66 +- src/core/settings.h | 10 + src/core/system.cpp | 28 +- src/core/types.h | 7 + src/duckstation-qt/biossettingswidget.cpp | 53 ++ src/duckstation-qt/biossettingswidget.h | 2 + src/duckstation-qt/biossettingswidget.ui | 64 +- src/util/state_wrapper.h | 1 + 18 files changed, 1182 insertions(+), 65 deletions(-) create mode 100644 src/core/pio.cpp create mode 100644 src/core/pio.h diff --git a/src/common/log_channels.h b/src/common/log_channels.h index 5ccab2409..af5744eac 100644 --- a/src/common/log_channels.h +++ b/src/common/log_channels.h @@ -50,6 +50,7 @@ X(PlatformMisc) \ X(PostProcessing) \ X(ProgressCallback) \ + X(PIO) \ X(ReShadeFXShader) \ X(Recompiler) \ X(SDL) \ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a3245020b..758d9afc2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -106,6 +106,8 @@ add_library(core pcdrv.h performance_counters.cpp performance_counters.h + pio.cpp + pio.h playstation_mouse.cpp playstation_mouse.h psf_loader.cpp diff --git a/src/core/bus.cpp b/src/core/bus.cpp index ddb3f7920..ea2fe9851 100644 --- a/src/core/bus.cpp +++ b/src/core/bus.cpp @@ -14,6 +14,7 @@ #include "interrupt_controller.h" #include "mdec.h" #include "pad.h" +#include "pio.h" #include "psf_loader.h" #include "settings.h" #include "sio.h" @@ -43,8 +44,6 @@ LOG_CHANNEL(Bus); -// TODO: Get rid of page code bits, instead use page faults to track SMC. - // Exports for external debugger access #ifndef __ANDROID__ namespace Exports { @@ -139,8 +138,6 @@ std::array g_bios_access_time = {}; std::array g_cdrom_access_time = {}; std::array g_spu_access_time = {}; -static std::vector s_exp1_rom; - static MEMCTRL s_MEMCTRL = {}; static RAM_SIZE_REG s_RAM_SIZE = {}; @@ -518,6 +515,8 @@ void Bus::RecalculateMemoryTimings() CalculateMemoryTiming(s_MEMCTRL.cdrom_delay_size, s_MEMCTRL.common_delay); std::tie(g_spu_access_time[0], g_spu_access_time[1], g_spu_access_time[2]) = CalculateMemoryTiming(s_MEMCTRL.spu_delay_size, s_MEMCTRL.common_delay); + std::tie(g_exp1_access_time[0], g_exp1_access_time[1], g_exp1_access_time[2]) = + CalculateMemoryTiming(s_MEMCTRL.exp1_delay_size, s_MEMCTRL.common_delay); TRACE_LOG("BIOS Memory Timing: {} bit bus, byte={}, halfword={}, word={}", s_MEMCTRL.bios_delay_size.data_bus_16bit ? 16 : 8, g_bios_access_time[0] + 1, g_bios_access_time[1] + 1, @@ -528,6 +527,9 @@ void Bus::RecalculateMemoryTimings() TRACE_LOG("SPU Memory Timing: {} bit bus, byte={}, halfword={}, word={}", s_MEMCTRL.spu_delay_size.data_bus_16bit ? 16 : 8, g_spu_access_time[0] + 1, g_spu_access_time[1] + 1, g_spu_access_time[2] + 1); + TRACE_LOG("EXP1 Memory Timing: {} bit bus, byte={}, halfword={}, word={}", + s_MEMCTRL.spu_delay_size.data_bus_16bit ? 16 : 8, g_spu_access_time[0] + 1, g_spu_access_time[1] + 1, + g_spu_access_time[2] + 1); } void* Bus::GetFastmemBase(bool isc) @@ -926,11 +928,6 @@ std::optional Bus::SearchMemory(PhysicalMemoryAddress sta return std::nullopt; } -void Bus::SetExpansionROM(std::vector data) -{ - s_exp1_rom = std::move(data); -} - void Bus::AddTTYCharacter(char ch) { if (ch == '\r') @@ -1503,53 +1500,49 @@ u32 Bus::EXP1ReadHandler(VirtualMemoryAddress address) { BUS_CYCLES(g_exp1_access_time[static_cast(size)]); + // TODO: auto-increment should be handled elsewhere... + const u32 offset = address & EXP1_MASK; - u32 value; - if (s_exp1_rom.empty()) + u32 ret; + + if constexpr (size >= MemoryAccessSize::HalfWord) { - // EXP1 not present. - value = UINT32_C(0xFFFFFFFF); - } - else if (offset == 0x20018) - { - // Bit 0 - Action Replay On/Off - value = UINT32_C(1); + ret = g_pio_device->ReadHandler(offset); + ret |= ZeroExtend32(g_pio_device->ReadHandler(offset + 1)) << 8; + if constexpr (size == MemoryAccessSize::Word) + { + ret |= ZeroExtend32(g_pio_device->ReadHandler(offset + 2)) << 16; + ret |= ZeroExtend32(g_pio_device->ReadHandler(offset + 3)) << 24; + } } else { - const u32 transfer_size = u32(1) << static_cast(size); - if ((offset + transfer_size) > s_exp1_rom.size()) - { - value = UINT32_C(0); - } - else - { - if constexpr (size == MemoryAccessSize::Byte) - { - value = ZeroExtend32(s_exp1_rom[offset]); - } - else if constexpr (size == MemoryAccessSize::HalfWord) - { - u16 halfword; - std::memcpy(&halfword, &s_exp1_rom[offset], sizeof(halfword)); - value = ZeroExtend32(halfword); - } - else - { - std::memcpy(&value, &s_exp1_rom[offset], sizeof(value)); - } - - // Log_DevPrintf("EXP1 read: 0x%08X -> 0x%08X", address, value); - } + ret = ZeroExtend32(g_pio_device->ReadHandler(offset)); } - return value; + return ret; } template void Bus::EXP1WriteHandler(VirtualMemoryAddress address, u32 value) { - WARNING_LOG("EXP1 write: 0x{:08X} <- 0x{:08X}", address, value); + // TODO: auto-increment should be handled elsewhere... + + const u32 offset = address & EXP1_MASK; + if constexpr (size >= MemoryAccessSize::HalfWord) + { + g_pio_device->WriteHandler(offset, Truncate8(value)); + g_pio_device->WriteHandler(offset + 1, Truncate8(value >> 8)); + if constexpr (size == MemoryAccessSize::Word) + { + g_pio_device->WriteHandler(offset + 2, Truncate8(value >> 16)); + g_pio_device->WriteHandler(offset + 3, Truncate8(value >> 24)); + } + } + else + { + g_pio_device->WriteHandler(offset, Truncate8(value)); + } } template diff --git a/src/core/bus.h b/src/core/bus.h index c95383e19..0c0e613e4 100644 --- a/src/core/bus.h +++ b/src/core/bus.h @@ -5,15 +5,11 @@ #include "types.h" -#include "common/bitfield.h" - #include #include #include #include -#include #include -#include class Error; @@ -148,8 +144,6 @@ void* GetFastmemBase(bool isc); void RemapFastmemViews(); bool CanUseFastmemForAddress(VirtualMemoryAddress address); -void SetExpansionROM(std::vector data); - extern std::bitset g_ram_code_bits; extern u8* g_ram; // 2MB-8MB RAM extern u8* g_unprotected_ram; // RAM without page protection, use for debugger access. diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 9917ef5f0..029dc81dc 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -69,6 +69,7 @@ + @@ -148,6 +149,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 1068324cc..32cdd1889 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -66,6 +66,7 @@ + @@ -141,6 +142,7 @@ + diff --git a/src/core/cpu_core.cpp b/src/core/cpu_core.cpp index db6be82dd..14eac79ae 100644 --- a/src/core/cpu_core.cpp +++ b/src/core/cpu_core.cpp @@ -10,6 +10,7 @@ #include "gte.h" #include "host.h" #include "pcdrv.h" +#include "pio.h" #include "settings.h" #include "system.h" #include "timing_event.h" @@ -2693,6 +2694,14 @@ ALWAYS_INLINE_RELEASE bool CPU::DoInstructionRead(PhysicalMemoryAddress address, return true; } + else if (address >= EXP1_BASE && address < (EXP1_BASE + EXP1_SIZE)) + { + g_pio_device->CodeReadHandler(address & EXP1_MASK, data, word_count); + if constexpr (add_ticks) + g_state.pending_ticks += g_exp1_access_time[static_cast(MemoryAccessSize::Word)] * word_count; + + return true; + } else { if (raise_exceptions) diff --git a/src/core/pio.cpp b/src/core/pio.cpp new file mode 100644 index 000000000..06608717e --- /dev/null +++ b/src/core/pio.cpp @@ -0,0 +1,863 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "pio.h" +#include "bus.h" +#include "settings.h" +#include "system.h" +#include "types.h" + +#include "util/state_wrapper.h" + +#include "common/assert.h" +#include "common/bitfield.h" +#include "common/bitutils.h" +#include "common/error.h" +#include "common/file_system.h" +#include "common/heap_array.h" +#include "common/log.h" +#include "common/path.h" + +#include + +LOG_CHANNEL(PIO); + +namespace PIO { + +// Atmel AT29C040A +class Flash +{ +public: + static constexpr u32 TOTAL_SIZE = 512 * 1024; + + Flash(); + ~Flash(); + + ALWAYS_INLINE bool IsImageModified() const { return m_image_modified; } + + bool LoadImage(const char* path, Error* error); + bool SaveImage(const char* path, Error* error); + + void Reset(); + bool DoState(StateWrapper& sw); + + u8 Read(u32 offset); + void CodeRead(u32 offset, u32* words, u32 word_count); + void Write(u32 offset, u8 value); + +private: + static constexpr u32 SECTOR_SIZE = 256; + static constexpr u32 SECTOR_COUNT = TOTAL_SIZE / SECTOR_SIZE; + static constexpr TickCount PROGRAM_TIMER_CYCLES = 5080; // ~150us + + enum : u8 + { + // 3 byte commands + FLASH_CMD_ENTER_ID_MODE = 0x90, + FLASH_CMD_EXIT_ID_MODE = 0xF0, + FLASH_CMD_WRITE_SECTOR_WITH_SDP = 0xA0, + FLASH_CMD_BEGIN_5_BYTE_COMMAND = 0x80, + + // 5 byte commands + FLASH_CMD_WRITE_SECTOR_WITHOUT_SDP = 0x20, + FLASH_CMD_ALT_ENTER_ID_MODE = 0x60, + }; + + u8* SectorPtr(u32 sector); + void PushCommandByte(u8 value); + + void ProgramWrite(u32 offset, u8 value); + bool CheckForProgramTimeout(); + void EndProgramming(); + + DynamicHeapArray m_data; + GlobalTicks m_program_write_timeout = 0; + std::array m_command_buffer = {}; + bool m_flash_id_mode = false; + bool m_write_enable = false; + bool m_image_modified = false; + u8 m_write_toggle_result = 0; + u16 m_write_position = 0; + u32 m_sector_address = 0; + u32 m_max_data_address = 0; +}; + +} // namespace PIO + +PIO::Flash::Flash() = default; + +PIO::Flash::~Flash() = default; + +u8 PIO::Flash::Read(u32 offset) +{ + if (m_flash_id_mode) [[unlikely]] + { + // Atmel AT29C040A + static constexpr std::array flash_id = {0x1F, 0xA4}; + return flash_id[offset & 1]; + } + + // WARNING_LOG("FLASH READ 0x{:X} 0x{:X} @ {}", offset, g_exp1_rom[offset], System::GetGlobalTickCounter()); + + if (m_write_enable && !CheckForProgramTimeout()) [[unlikely]] + { + m_write_toggle_result ^= 0x40; + WARNING_LOG("read while programming 0x{:02X}", m_write_toggle_result); + EndProgramming(); + return m_write_toggle_result | 0x80; + } + + return (offset >= TOTAL_SIZE) ? 0xFFu : m_data[offset]; +} + +void PIO::Flash::CodeRead(u32 offset, u32* words, u32 word_count) +{ + DebugAssert((offset + (word_count * sizeof(u32))) < TOTAL_SIZE); + std::memcpy(words, m_data.data() + offset, word_count * sizeof(u32)); +} + +void PIO::Flash::PushCommandByte(u8 value) +{ + for (u32 i = static_cast(std::size(m_command_buffer) - 1); i > 0; i--) + m_command_buffer[i] = m_command_buffer[i - 1]; + m_command_buffer[0] = value; +} + +void PIO::Flash::Write(u32 offset, u8 value) +{ + if (m_write_enable && !CheckForProgramTimeout()) + { + ProgramWrite(offset, value); + return; + } + + DEV_LOG("FLASH WRITE 0x{:X} 0x{:X}", offset, value); + + // Ignore banked addresses + offset &= 0x3FFFF; + if (offset == 0x2AAA || offset == 0x5555) + { + PushCommandByte(value); + + const auto& buf = m_command_buffer; + if (buf[2] == 0xAA && buf[1] == 0x55) + { + if (value == FLASH_CMD_ENTER_ID_MODE) + { + DEV_LOG("Flash enter ID mode"); + m_flash_id_mode = true; + } + else if (value == FLASH_CMD_EXIT_ID_MODE) + { + DEV_LOG("Flash exit ID mode"); + m_flash_id_mode = false; + } + else if (value == FLASH_CMD_WRITE_SECTOR_WITH_SDP) + { + DEV_LOG("Flash write sector with SDP @ {}", System::GetGlobalTickCounter()); + m_write_enable = true; + m_program_write_timeout = System::GetGlobalTickCounter() + PROGRAM_TIMER_CYCLES; + } + else if (buf[5] == 0xAA && buf[4] == 0x55 && buf[3] == 0x80) + { + if (value == FLASH_CMD_ALT_ENTER_ID_MODE) + { + DEV_LOG("Flash Alt Enter ID mode"); + m_flash_id_mode = true; + } + if (value == FLASH_CMD_WRITE_SECTOR_WITHOUT_SDP) + { + DEV_LOG("Flash Write sector WITHOUT SDP"); + m_write_enable = true; + m_program_write_timeout = std::numeric_limits::max(); + } + else + { + ERROR_LOG("Unhandled 5-cycle flash command 0x{:02X}", value); + } + } + else if (value != 0x80) + { + ERROR_LOG("Unhandled 3-cycle flash command 0x{:02X}", value); + } + } + } +} + +void PIO::Flash::ProgramWrite(u32 offset, u8 value) +{ + // reset the timeout.. event system suckage, we need it from _this_ cycle, not the first + m_program_write_timeout = std::max(m_program_write_timeout, System::GetGlobalTickCounter() + PROGRAM_TIMER_CYCLES); + + static_assert((0x800 * 0x100) == TOTAL_SIZE); + const u32 byte_address = (offset & 0xFFu); + const u32 sector_address = (offset >> 8) & 0x7FFu; + if (m_write_position == 0) + { + DEV_LOG("Writing to flash sector {} (offset 0x{:06X})", sector_address, sector_address * SECTOR_SIZE); + m_sector_address = sector_address; + + const u32 sector_data_end = (sector_address * SECTOR_SIZE) + SECTOR_SIZE; + if (sector_data_end > m_max_data_address) + { + m_max_data_address = sector_data_end; + m_image_modified = true; + } + } + + if (sector_address == m_sector_address) [[likely]] + { + u8* byte_ptr = SectorPtr(sector_address) + byte_address; + m_image_modified |= (*byte_ptr != value); + *byte_ptr = value; + } + else + { + WARNING_LOG("Flash write: unexpected sector address of {}, expected {} (addr 0x{:05X}", sector_address, + m_sector_address, offset); + } + + m_write_position++; + if (m_write_position == SECTOR_SIZE) + { + // end of flash write + EndProgramming(); + } +} + +bool PIO::Flash::CheckForProgramTimeout() +{ + DebugAssert(m_write_enable); + if (System::GetGlobalTickCounter() < m_program_write_timeout) + return false; + + WARNING_LOG("Flash program timeout at byte {}", m_write_position); + + // kinda cheating here, the sector would normally get buffered and then written + // but the flash isn't supposed to be readable during programming anyway... + if (m_write_position > 0) + { + const u32 bytes_to_erase = SECTOR_SIZE - m_write_position; + if (bytes_to_erase > 0) + { + WARNING_LOG("Erasing {} unwritten bytes in sector {} (0x{:05X})", bytes_to_erase, m_write_position, + m_sector_address * SECTOR_SIZE); + + u8* sector = SectorPtr(m_sector_address) + m_write_position; + bool image_modified = false; + for (u32 i = 0; i < bytes_to_erase; i++) + { + image_modified |= (sector[i] != 0xFF); + sector[i] = 0xFF; + } + m_image_modified |= image_modified; + } + } + else + { + WARNING_LOG("No sector address set, skipping programming."); + } + + EndProgramming(); + return true; +} + +void PIO::Flash::EndProgramming() +{ + m_write_enable = false; + m_write_position = 0; + m_sector_address = 0; + m_write_toggle_result = 0; +} + +u8* PIO::Flash::SectorPtr(u32 sector) +{ + DebugAssert(sector < SECTOR_COUNT); + return (m_data.data() + sector * SECTOR_SIZE); +} + +bool PIO::Flash::LoadImage(const char* path, Error* error) +{ + const FileSystem::ManagedCFilePtr fp = FileSystem::OpenManagedCFile(path, "rb", error); + if (!fp) + { + Error::AddPrefixFmt(error, "Failed to open PIO flash image '{}': ", Path::GetFileName(path)); + return false; + } + + const s64 file_size = std::max(FileSystem::FSize64(fp.get(), error), 0); + if (file_size > TOTAL_SIZE) + { + WARNING_LOG("PIO flash image is too large ({} bytes), only {} bytes will be read.", file_size, TOTAL_SIZE); + } + else if (file_size < TOTAL_SIZE) + { + DEV_LOG("PIO flash image is too small ({} bytes), {} bytes of padding will be added.", file_size, + TOTAL_SIZE - file_size); + } + + const u32 read_size = static_cast(std::min(file_size, TOTAL_SIZE)); + m_data.resize(TOTAL_SIZE); + if (read_size > 0 && std::fread(m_data.data(), read_size, 1, fp.get()) != 1) + { + Error::SetErrno(error, "Failed to read PIO flash image: ", errno); + m_data.deallocate(); + return false; + } + + const u32 padding_size = TOTAL_SIZE - read_size; + if (padding_size > 0) + std::memset(m_data.data() + read_size, 0, padding_size); + + m_max_data_address = read_size; + return true; +} + +bool PIO::Flash::SaveImage(const char* path, Error* error) +{ + WARNING_LOG("Writing PIO flash image '{}'", Path::GetFileName(path)); + + if (!FileSystem::WriteBinaryFile(path, m_data.cspan(0, m_max_data_address), error)) + { + Error::AddPrefixFmt(error, "Failed to write PIO flash image '{}': ", Path::GetFileName(path)); + return false; + } + + return true; +} + +void PIO::Flash::Reset() +{ + m_command_buffer.fill(0); + m_flash_id_mode = false; + m_write_enable = false; + m_write_position = 0; + m_sector_address = 0; + m_program_write_timeout = 0; + m_write_toggle_result = 0; +} + +bool PIO::Flash::DoState(StateWrapper& sw) +{ + sw.DoBytes(m_data.data(), m_data.size()); + sw.DoBytes(m_command_buffer.data(), m_command_buffer.size()); + sw.Do(&m_flash_id_mode); + sw.Do(&m_write_enable); + sw.Do(&m_write_position); + sw.Do(&m_sector_address); + sw.Do(&m_program_write_timeout); + sw.Do(&m_write_toggle_result); + sw.Do(&m_image_modified); + sw.Do(&m_max_data_address); + return !sw.HasError(); +} + +namespace PIO { + +class NullDevice : public Device +{ +public: + NullDevice(); + ~NullDevice() override; + + bool Initialize(Error* error) override; + + void Reset() override; + void UpdateSettings(const Settings& old_settings) override; + bool DoState(StateWrapper& sw) override; + + u8 ReadHandler(u32 offset) override; + void CodeReadHandler(u32 offset, u32* words, u32 word_count) override; + void WriteHandler(u32 offset, u8 value) override; +}; + +} // namespace PIO + +PIO::NullDevice::NullDevice() = default; + +PIO::NullDevice::~NullDevice() = default; + +bool PIO::NullDevice::Initialize(Error* error) +{ + return true; +} + +void PIO::NullDevice::Reset() +{ +} + +void PIO::NullDevice::UpdateSettings(const Settings& old_settings) +{ +} + +bool PIO::NullDevice::DoState(StateWrapper& sw) +{ + return true; +} + +u8 PIO::NullDevice::ReadHandler(u32 offset) +{ + return 0xFFu; +} + +void PIO::NullDevice::CodeReadHandler(u32 offset, u32* words, u32 word_count) +{ + std::memset(words, 0xFF, sizeof(u32) * word_count); +} + +void PIO::NullDevice::WriteHandler(u32 offset, u8 value) +{ +} + +#if 0 + +namespace PIO { + +namespace { + +class DatelCartDevice : public Device +{ +public: + DatelCartDevice(); + ~DatelCartDevice() override; + + bool Initialize(Error* error) override; + + void Reset() override; + void UpdateSettings(const Settings& old_settings) override; + bool DoState(StateWrapper& sw) override; + + u8 ReadHandler(u32 offset) override; + void CodeReadHandler(u32 offset, u32* words, u32 word_count) override; + void WriteHandler(u32 offset, u8 value) override; + +private: + Flash m_flash; +}; + +} + +} // namespace PIO + +PIO::DatelCartDevice::DatelCartDevice() = default; + +PIO::DatelCartDevice::~DatelCartDevice() = default; + +bool PIO::DatelCartDevice::Initialize(Error* error) +{ + return false; +} + +void PIO::DatelCartDevice::Reset() +{ +} + +void PIO::DatelCartDevice::UpdateSettings(const Settings& old_settings) +{ +} + +bool PIO::DatelCartDevice::DoState(StateWrapper& sw) +{ + return false; +} + +u8 PIO::DatelCartDevice::ReadHandler(u32 offset) +{ + WARNING_LOG("Datel EXP1 read 0x{:08X}", offset); + + if (offset < 0x20000) + { + // first 128KB of flash + return m_flash.Read(offset); + } + else if (offset >= 0x40000 && offset < 0x60000) // 1F040000->1F05FFFF + { + // second 128KB of flash + return m_flash.Read((offset - 0x40000) + 0x20000); + } + else if (offset == 0x20018) + { + // switch setting + return 1u; + } + else if (offset == 0x20010) + { + // comms link STB pin state (bit 0) + return 0u; + } + else if (offset == 0x60000) + { + // comms link data in + return 0u; + } + else + { + WARNING_LOG("Unhandled Datel EXP1 read: 0x{:08X}", offset); + return 0xFFu; + } +} + +void PIO::DatelCartDevice::CodeReadHandler(u32 offset, u32* words, u32 word_count) +{ + if (offset < 0x20000) + m_flash.CodeRead(offset, words, word_count); + else if (offset >= 0x40000 && offset < 0x60000) // 1F040000->1F05FFFF + m_flash.CodeRead((offset - 0x40000) + 0x20000, words, word_count); + else + std::memset(words, 0xFF, sizeof(u32) * word_count); +} + +void PIO::DatelCartDevice::WriteHandler(u32 offset, u8 value) +{ + WARNING_LOG("DATEL WRITE 0x{:08X} 0x{:08X}", offset, value); +} + +#endif + +// Xplorer/Xploder +namespace PIO { + +namespace { + +class XplorerCart : public Device +{ +public: + static constexpr u32 SRAM_SIZE = 128 * 1024; + + XplorerCart(); + ~XplorerCart() override; + + bool Initialize(Error* error) override; + void UpdateSettings(const Settings& old_settings) override; + + void Reset() override; + bool DoState(StateWrapper& sw) override; + + u8 ReadHandler(u32 offset) override; + void CodeReadHandler(u32 offset, u32* words, u32 word_count) override; + void WriteHandler(u32 offset, u8 value) override; + +private: + ALWAYS_INLINE u32 GetFlashUpperBank() const { return m_memory_map.sram_bank ? (384 * 1024) : (256 * 1024); } + + union MemoryMappingRegister + { + u8 bits; + + BitField pc_slct; + BitField pc_pe; + BitField pc_busy; + BitField pc_ack; + BitField sram_select; + BitField flash_bank; + BitField sram_bank; + BitField sram_bank_2; + }; + + DynamicHeapArray m_sram; + Flash m_flash; + MemoryMappingRegister m_memory_map = {}; + bool m_switch_state = false; +}; + +} // namespace + +} // namespace PIO + +PIO::XplorerCart::XplorerCart() +{ + m_sram.resize(SRAM_SIZE); +} + +PIO::XplorerCart::~XplorerCart() +{ + if (g_settings.pio_flash_write_enable && m_flash.IsImageModified()) + { + Error error; + if (!m_flash.SaveImage(g_settings.pio_flash_image_path.c_str(), &error)) + ERROR_LOG("Failed to update Xplorer flash image: {}", error.GetDescription()); + } +} + +bool PIO::XplorerCart::Initialize(Error* error) +{ + if (!m_flash.LoadImage(g_settings.pio_flash_image_path.c_str(), error)) + return false; + + m_switch_state = g_settings.pio_switch_active; + return true; +} + +void PIO::XplorerCart::UpdateSettings(const Settings& old_settings) +{ + m_switch_state = g_settings.pio_switch_active; +} + +void PIO::XplorerCart::Reset() +{ + m_flash.Reset(); + std::memset(m_sram.data(), 0, m_sram.size()); + m_memory_map.bits = 0; +} + +bool PIO::XplorerCart::DoState(StateWrapper& sw) +{ + m_flash.DoState(sw); + sw.DoBytes(m_sram.data(), m_sram.size()); + sw.Do(&m_memory_map.bits); + return !sw.HasError(); +} + +u8 PIO::XplorerCart::ReadHandler(u32 offset) +{ + // WARNING_LOG("Xplorer EXP1 read size {}: 0x{:08X}", 1u << (u32)size, address); + + if (offset < 0x40000) // 1F000000->1F03FFFF + { + // first 256KB of flash + return m_flash.Read(offset); + } + else if (offset < 0x60000) // 1F040000->1F05FFFF + { + // second 256KB of flash or SRAM + offset &= 0x3FFFF; + if (m_memory_map.sram_select) + { + DebugAssert(offset < SRAM_SIZE); + return m_sram[offset]; + } + else + { + return m_flash.Read(offset | GetFlashUpperBank()); + } + } + else if (offset >= 0x60000 && offset < 0x70000) + { + // I/O, mirrored + switch (offset & 0x07) + { + case 0: + { + // switch setting + return 0xFEu | BoolToUInt8(m_switch_state); + } + + case 1: + { + // data from PC + return 0u; + } + + case 2: + { + // handshake from PC + return 0xFEu; + } + + case 3: + case 4: + case 5: + case 6: + case 7: + { + // unknown + WARNING_LOG("Unhandled Xplorer I/O register read: 0x{:08X}", offset); + return 0xFFu; + } + + DefaultCaseIsUnreachable() + } + } + else + { + WARNING_LOG("Unhandled Xplorer EXP1 read: 0x{:08X}", offset); + return 0xFFu; + } +} + +void PIO::XplorerCart::CodeReadHandler(u32 offset, u32* words, u32 word_count) +{ + if ((offset + word_count) <= 0x40000) + { + m_flash.CodeRead(offset, words, word_count); + } + else if (offset >= 0x40000 && (offset + word_count) <= 0x60000) + { + // second 256KB of flash or SRAM + const u32 bank_offset = offset - 0x40000; + if (m_memory_map.sram_select) + { + DebugAssert(bank_offset < SRAM_SIZE); + std::memcpy(words, &m_sram[bank_offset], word_count * sizeof(u32)); + } + else + { + m_flash.CodeRead(bank_offset, words, word_count * sizeof(u32)); + } + } + else if (offset < 0x60000) [[unlikely]] + { + // partial read of both banks + if (offset < 0x40000) + { + const u32 words_from_first = (0x40000 - offset) / sizeof(u32); + m_flash.CodeRead(offset, words, words_from_first); + words += words_from_first; + word_count -= words_from_first; + offset += words_from_first * sizeof(u32); + } + + const u32 words_from_second = std::min(0x60000 - offset, word_count); + const u32 second_bank_offset = offset - 0x40000; + if (m_memory_map.sram_bank) + { + std::memcpy(words, &m_sram[second_bank_offset], words_from_second * sizeof(u32)); + } + else + { + m_flash.CodeRead(second_bank_offset + GetFlashUpperBank(), words, words_from_second); + } + + words += words_from_second; + word_count -= words_from_second; + if (word_count > 0) + std::memset(words, 0xFF, sizeof(u32) * word_count); + } + else + { + std::memset(words, 0xFF, sizeof(u32) * word_count); + } +} + +void PIO::XplorerCart::WriteHandler(u32 offset, u8 value) +{ + if (offset < 0x40000) + { + m_flash.Write(offset, value); + } + else if (offset < 0x60000) + { + const u32 bank_offset = offset - 0x40000; + if (m_memory_map.sram_bank) + { + m_sram[bank_offset] = value; + } + else + { + const u32 flash_offset = m_memory_map.sram_bank ? (384 * 1024) : (256 * 1024); + m_flash.Write(flash_offset + bank_offset, value); + } + } + else if (offset == 0x60001) + { + DEV_LOG("Memory map <- 0x{:02X}", value); + m_memory_map.bits = value; + } + else + { + WARNING_LOG("Unhandled Xplorer WRITE 0x{:08X} 0x{:08X}", offset, value); + } +} + +namespace PIO { + +static std::unique_ptr CreateDevice(PIODeviceType type); + +} // namespace PIO + +std::unique_ptr g_pio_device; + +PIO::Device::~Device() = default; + +bool PIO::Initialize(Error* error) +{ + g_pio_device = CreateDevice(g_settings.pio_device_type); + Assert(g_pio_device); + + if (!g_pio_device->Initialize(error)) + { + g_pio_device.reset(); + return false; + } + + return true; +} + +void PIO::UpdateSettings(const Settings& old_settings) +{ + if (g_settings.pio_device_type != old_settings.pio_device_type) + { + Error error; + g_pio_device.reset(); + g_pio_device = CreateDevice(g_settings.pio_device_type); + if (!g_pio_device->Initialize(&error)) + { + ERROR_LOG("Failed to create new PIO device: {}", error.GetDescription()); + g_pio_device = CreateDevice(PIODeviceType::None); + } + } + else + { + g_pio_device->UpdateSettings(old_settings); + } +} + +void PIO::Shutdown() +{ + g_pio_device.reset(); +} + +std::unique_ptr PIO::CreateDevice(PIODeviceType type) +{ + switch (type) + { + case PIODeviceType::None: + return std::make_unique(); + + case PIODeviceType::XplorerCart: + return std::make_unique(); + + default: + return nullptr; + } +} + +void PIO::Reset() +{ + g_pio_device->Reset(); +} + +bool PIO::DoState(StateWrapper& sw) +{ + PIODeviceType device_type = g_settings.pio_device_type; + sw.Do(&device_type); + + const size_t pio_state_pos = sw.GetPosition(); + u32 pio_state_size = 0; + sw.Do(&pio_state_size); + + if (device_type == g_settings.pio_device_type) [[likely]] + { + if (!g_pio_device->DoState(sw)) + return false; + + // rewrite size field + if (sw.IsWriting()) + { + const size_t new_pos = sw.GetPosition(); + sw.SetPosition(pio_state_pos); + pio_state_size = static_cast(new_pos - pio_state_pos); + sw.Do(&pio_state_size); + sw.SetPosition(new_pos); + } + } + else + { + WARNING_LOG("State contains PIO device {}, expected {}", Settings::GetPIODeviceTypeModeName(device_type), + Settings::GetPIODeviceTypeModeName(g_settings.pio_device_type)); + g_pio_device->Reset(); + sw.SkipBytes(pio_state_size); + } + + return !sw.HasError(); +} diff --git a/src/core/pio.h b/src/core/pio.h new file mode 100644 index 000000000..ecdadb384 --- /dev/null +++ b/src/core/pio.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "types.h" + +#include + +class Error; + +class StateWrapper; + +struct Settings; + +enum class PIODevice : u8; + +namespace PIO { + +// Exposed so Bus can call the handlers directly. Otherwise calls should go through the functions below. + +class Device +{ +public: + virtual ~Device(); + + virtual bool Initialize(Error* error) = 0; + virtual void UpdateSettings(const Settings& old_settings) = 0; + + virtual void Reset() = 0; + + virtual bool DoState(StateWrapper& sw) = 0; + + virtual u8 ReadHandler(u32 offset) = 0; + virtual void CodeReadHandler(u32 offset, u32* words, u32 word_count) = 0; + virtual void WriteHandler(u32 offset, u8 value) = 0; +}; + +extern bool Initialize(Error* error); +extern void UpdateSettings(const Settings& old_settings); +extern void Shutdown(); + +extern void Reset(); +extern bool DoState(StateWrapper& sw); + +} // namespace PIO + +extern std::unique_ptr g_pio_device; diff --git a/src/core/save_state_version.h b/src/core/save_state_version.h index c3cc9d4d3..f404f7925 100644 --- a/src/core/save_state_version.h +++ b/src/core/save_state_version.h @@ -6,7 +6,7 @@ #include "common/types.h" static constexpr u32 SAVE_STATE_MAGIC = 0x43435544; -static constexpr u32 SAVE_STATE_VERSION = 76; +static constexpr u32 SAVE_STATE_VERSION = 77; 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 75d0f86f3..b4ce76d1f 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -361,9 +361,6 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro use_old_mdec_routines = si.GetBoolValue("Hacks", "UseOldMDECRoutines", false); export_shared_memory = si.GetBoolValue("Hacks", "ExportSharedMemory", false); - pcdrv_enable = si.GetBoolValue("PCDrv", "Enabled", false); - pcdrv_enable_writes = si.GetBoolValue("PCDrv", "EnableWrites", false); - pcdrv_root = si.GetStringValue("PCDrv", "Root"); dma_max_slice_ticks = si.GetIntValue("Hacks", "DMAMaxSliceTicks", DEFAULT_DMA_MAX_SLICE_TICKS); dma_halt_ticks = si.GetIntValue("Hacks", "DMAHaltTicks", DEFAULT_DMA_HALT_TICKS); @@ -481,6 +478,17 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro texture_replacements.config.vram_write_dump_height_threshold = si.GetUIntValue("TextureReplacements", "DumpVRAMWriteHeightThreshold", 128); + pio_device_type = ParsePIODeviceTypeName( + si.GetTinyStringValue("PIO", "DeviceType", GetPIODeviceTypeModeName(DEFAULT_PIO_DEVICE_TYPE))) + .value_or(DEFAULT_PIO_DEVICE_TYPE); + pio_flash_image_path = si.GetStringValue("PIO", "FlashImagePath"); + pio_flash_write_enable = si.GetBoolValue("PIO", "FlashImageWriteEnable", false); + pio_switch_active = si.GetBoolValue("PIO", "SwitchActive", true); + + pcdrv_enable = si.GetBoolValue("PCDrv", "Enabled", false); + pcdrv_enable_writes = si.GetBoolValue("PCDrv", "EnableWrites", false); + pcdrv_root = si.GetStringValue("PCDrv", "Root"); + #ifdef __ANDROID__ // Android users are incredibly silly and don't understand that stretch is in the aspect ratio list... if (si.GetBoolValue("Display", "Stretch", false)) @@ -649,10 +657,6 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const si.SetIntValue("Hacks", "GPUMaxRunAhead", gpu_max_run_ahead); } - si.SetBoolValue("PCDrv", "Enabled", pcdrv_enable); - si.SetBoolValue("PCDrv", "EnableWrites", pcdrv_enable_writes); - si.SetStringValue("PCDrv", "Root", pcdrv_root.c_str()); - si.SetBoolValue("BIOS", "TTYLogging", bios_tty_logging); si.SetBoolValue("BIOS", "PatchFastBoot", bios_patch_fast_boot); si.SetBoolValue("BIOS", "FastForwardBoot", bios_fast_forward_boot); @@ -746,6 +750,15 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const texture_replacements.config.vram_write_dump_width_threshold); si.SetUIntValue("TextureReplacements", "DumpVRAMWriteHeightThreshold", texture_replacements.config.vram_write_dump_height_threshold); + + si.SetStringValue("PIO", "DeviceType", GetPIODeviceTypeModeName(pio_device_type)); + si.SetStringValue("PIO", "FlashImagePath", pio_flash_image_path.c_str()); + si.SetBoolValue("PIO", "FlashImageWriteEnable", pio_flash_write_enable); + si.SetBoolValue("PIO", "SwitchActive", pio_switch_active); + + si.SetBoolValue("PCDrv", "Enabled", pcdrv_enable); + si.SetBoolValue("PCDrv", "EnableWrites", pcdrv_enable_writes); + si.SetStringValue("PCDrv", "Root", pcdrv_root.c_str()); } void Settings::Clear(SettingsInterface& si) @@ -964,10 +977,11 @@ void Settings::FixIncompatibleSettings(bool display_osd_messages) g_settings.cdrom_mute_cd_audio = false; g_settings.texture_replacements.enable_vram_write_replacements = false; g_settings.use_old_mdec_routines = false; - g_settings.pcdrv_enable = false; g_settings.bios_patch_fast_boot = false; g_settings.runahead_frames = 0; g_settings.rewind_enable = false; + g_settings.pio_device_type = PIODeviceType::None; + g_settings.pcdrv_enable = false; } // fast forward boot requires fast boot @@ -2176,6 +2190,42 @@ const char* Settings::GetSaveStateCompressionModeDisplayName(SaveStateCompressio "SaveStateCompressionMode"); } +static constexpr const std::array s_pio_device_type_names = { + "None", + "XplorerCart", +}; +static constexpr const std::array s_pio_device_type_display_names = { + TRANSLATE_DISAMBIG_NOOP("Settings", "None", "PIODeviceType"), + TRANSLATE_DISAMBIG_NOOP("Settings", "Xplorer/Xploder Cartridge", "PIODeviceType"), +}; +static_assert(s_pio_device_type_names.size() == static_cast(PIODeviceType::MaxCount)); +static_assert(s_pio_device_type_display_names.size() == static_cast(PIODeviceType::MaxCount)); + +std::optional Settings::ParsePIODeviceTypeName(const char* str) +{ + u32 index = 0; + for (const char* name : s_pio_device_type_names) + { + if (StringUtil::Strcasecmp(name, str) == 0) + return static_cast(index); + + index++; + } + + return std::nullopt; +} + +const char* Settings::GetPIODeviceTypeModeName(PIODeviceType type) +{ + return s_pio_device_type_names[static_cast(type)]; +} + +const char* Settings::GetPIODeviceTypeModeDisplayName(PIODeviceType type) +{ + return Host::TranslateToCString("Settings", s_pio_device_type_display_names[static_cast(type)], + "PIODeviceType"); +} + std::string EmuFolders::AppRoot; std::string EmuFolders::DataRoot; std::string EmuFolders::Bios; diff --git a/src/core/settings.h b/src/core/settings.h index cdb1e5f57..a0af31968 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -297,6 +297,11 @@ struct Settings MultitapMode multitap_mode = DEFAULT_MULTITAP_MODE; + PIODeviceType pio_device_type = DEFAULT_PIO_DEVICE_TYPE; + std::string pio_flash_image_path; + bool pio_switch_active = true; + bool pio_flash_write_enable = false; + std::string pcdrv_root; bool pcdrv_enable_writes = false; @@ -486,6 +491,10 @@ struct Settings static const char* GetSaveStateCompressionModeName(SaveStateCompressionMode mode); static const char* GetSaveStateCompressionModeDisplayName(SaveStateCompressionMode mode); + static std::optional ParsePIODeviceTypeName(const char* str); + static const char* GetPIODeviceTypeModeName(PIODeviceType type); + static const char* GetPIODeviceTypeModeDisplayName(PIODeviceType type); + static constexpr GPURenderer DEFAULT_GPU_RENDERER = GPURenderer::Automatic; static constexpr GPUTextureFilter DEFAULT_GPU_TEXTURE_FILTER = GPUTextureFilter::Nearest; static constexpr GPULineDetectMode DEFAULT_GPU_LINE_DETECT_MODE = GPULineDetectMode::Disabled; @@ -535,6 +544,7 @@ struct Settings 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 PIODeviceType DEFAULT_PIO_DEVICE_TYPE = PIODeviceType::None; static constexpr s32 DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME = 5; static constexpr s32 DEFAULT_LEADERBOARD_NOTIFICATION_TIME = 10; diff --git a/src/core/system.cpp b/src/core/system.cpp index d59718a53..514063138 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -29,6 +29,7 @@ #include "pad.h" #include "pcdrv.h" #include "performance_counters.h" +#include "pio.h" #include "psf_loader.h" #include "save_state_version.h" #include "sio.h" @@ -1926,6 +1927,9 @@ bool System::Initialize(std::unique_ptr disc, DiscRegion disc_region, b CPU::Initialize(); CDROM::Initialize(); + if (!PIO::Initialize(error)) + return false; + // CDROM before GPU, that way we don't modeswitch. if (disc && !CDROM::InsertMedia(std::move(disc), disc_region, s_state.running_game_serial, s_state.running_game_title, error)) @@ -2011,6 +2015,7 @@ void System::DestroySystem() CDROM::Shutdown(); g_gpu.reset(); DMA::Shutdown(); + PIO::Shutdown(); CPU::CodeCache::Shutdown(); CPU::PGXP::Shutdown(); CPU::Shutdown(); @@ -2548,6 +2553,12 @@ bool System::DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_di if (!sw.DoMarker("SIO") || !SIO::DoState(sw)) return false; + if (sw.GetVersion() >= 77) + { + if (!sw.DoMarker("PIO") || !PIO::DoState(sw)) + return false; + } + if (!sw.DoMarker("Events") || !TimingEvents::DoState(sw)) return false; @@ -2628,6 +2639,7 @@ void System::InternalReset() CPU::PGXP::Initialize(); Bus::Reset(); + PIO::Reset(); DMA::Reset(); InterruptController::Reset(); g_gpu->Reset(true); @@ -4491,6 +4503,17 @@ void System::CheckForSettingsChanges(const Settings& old_settings) UpdateSpeedLimiterState(); } + if (g_settings.multitap_mode != old_settings.multitap_mode) + UpdateMultitaps(); + + if (g_settings.pio_device_type != old_settings.pio_device_type || + g_settings.pio_flash_image_path != old_settings.pio_flash_image_path || + g_settings.pio_flash_write_enable != old_settings.pio_flash_write_enable || + g_settings.pio_switch_active != old_settings.pio_switch_active) + { + PIO::UpdateSettings(old_settings); + } + if (g_settings.display_show_gpu_usage != old_settings.display_show_gpu_usage) g_gpu_device->SetGPUTimingEnabled(g_settings.display_show_gpu_usage); @@ -4537,9 +4560,6 @@ void System::CheckForSettingsChanges(const Settings& old_settings) ImGuiManager::SetScreenMargin(g_settings.display_osd_margin); } - if (g_settings.multitap_mode != old_settings.multitap_mode) - UpdateMultitaps(); - Achievements::UpdateSettings(old_settings); FullscreenUI::CheckForConfigChanges(old_settings); @@ -4739,6 +4759,8 @@ void System::WarnAboutUnsafeSettings() APPEND_SUBMESSAGE(TRANSLATE_SV("System", "VRAM write texture replacements disabled.")); if (g_settings.use_old_mdec_routines) APPEND_SUBMESSAGE(TRANSLATE_SV("System", "Use old MDEC routines disabled.")); + if (g_settings.pio_device_type != PIODeviceType::None) + APPEND_SUBMESSAGE(TRANSLATE_SV("System", "PIO device removed.")); if (g_settings.pcdrv_enable) APPEND_SUBMESSAGE(TRANSLATE_SV("System", "PCDrv disabled.")); if (g_settings.bios_patch_fast_boot) diff --git a/src/core/types.h b/src/core/types.h index 90eb27b7c..3b1ab0773 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -303,3 +303,10 @@ enum class ForceVideoTimingMode : u8 Count, }; + +enum class PIODeviceType : u8 +{ + None, + XplorerCart, + MaxCount, +}; diff --git a/src/duckstation-qt/biossettingswidget.cpp b/src/duckstation-qt/biossettingswidget.cpp index 78c0de9f0..59447e67d 100644 --- a/src/duckstation-qt/biossettingswidget.cpp +++ b/src/duckstation-qt/biossettingswidget.cpp @@ -10,6 +10,7 @@ #include "core/bios.h" #include "core/settings.h" +#include #include #include @@ -25,13 +26,37 @@ BIOSSettingsWidget::BIOSSettingsWidget(SettingsWindow* dialog, QWidget* parent) connect(m_ui.fastBoot, &QCheckBox::checkStateChanged, this, &BIOSSettingsWidget::onFastBootChanged); + SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.pioDeviceType, "PIO", "DeviceType", + &Settings::ParsePIODeviceTypeName, &Settings::GetPIODeviceTypeModeName, + &Settings::GetPIODeviceTypeModeDisplayName, + Settings::DEFAULT_PIO_DEVICE_TYPE, PIODeviceType::MaxCount); + SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.pioImagePath, "PIO", "FlashImagePath"); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pioSwitchActive, "PIO", "SwitchActive", true); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pioImageWrites, "PIO", "FlashImageWriteEnable", false); + connect(m_ui.pioDeviceType, QOverload::of(&QComboBox::currentIndexChanged), this, + &BIOSSettingsWidget::onPIODeviceTypeChanged); + connect(m_ui.pioImagePathBrowse, &QPushButton::clicked, this, &BIOSSettingsWidget::onPIOImagePathBrowseClicked); + onFastBootChanged(); + onPIODeviceTypeChanged(); dialog->registerWidgetHelp(m_ui.fastBoot, tr("Fast Boot"), tr("Unchecked"), tr("Patches the BIOS to skip the console's boot animation. Does not work with all games, " "but usually safe to enable.")); dialog->registerWidgetHelp(m_ui.enableTTYLogging, tr("Enable TTY Logging"), tr("Unchecked"), tr("Logs BIOS calls to printf(). Not all games contain debugging messages.")); + dialog->registerWidgetHelp(m_ui.pioDeviceType, tr("Device Type"), tr("None"), + tr("Simulates a device plugged into the console's parallel port. Usually these are flash " + "cartridges, and require some sort of image dump to function.")); + dialog->registerWidgetHelp(m_ui.pioImagePath, tr("Image Path"), tr("Empty"), + tr("Sets the path to the image used for flash cartridges.")); + dialog->registerWidgetHelp(m_ui.pioSwitchActive, tr("Cartridge Switch On"), tr("Checked"), + tr("Simulates the position of the switch on the cartridge. Most cartridges require the " + "switch to be on for it to activate on startup.")); + dialog->registerWidgetHelp( + m_ui.pioImageWrites, tr("Allow Image Writes"), tr("None"), + tr("Stores any images made to the cartridge's flash storage back to the host's file system. This will " + "overwrite your cartridge dump, you should ensure you have a backup first.")); connect(m_ui.imageNTSCJ, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { if (m_dialog->isPerGameSettings() && index == 0) @@ -180,3 +205,31 @@ void BIOSSettingsWidget::setDropDownValue(QComboBox* cb, const std::optionaladdItem(qname, QVariant(qname)); cb->setCurrentIndex(cb->count() - 1); } + +void BIOSSettingsWidget::onPIODeviceTypeChanged() +{ + const PIODeviceType type = + Settings::ParsePIODeviceTypeName( + m_dialog + ->getEffectiveStringValue("PIO", "DeviceType", + Settings::GetPIODeviceTypeModeName(Settings::DEFAULT_PIO_DEVICE_TYPE)) + .c_str()) + .value_or(Settings::DEFAULT_PIO_DEVICE_TYPE); + const bool has_image = (type == PIODeviceType::XplorerCart); + const bool has_switch = (type == PIODeviceType::XplorerCart); + m_ui.pioImagePathLabel->setEnabled(has_image); + m_ui.pioImagePath->setEnabled(has_image); + m_ui.pioImagePathBrowse->setEnabled(has_image); + m_ui.pioImageWrites->setEnabled(has_image); + m_ui.pioSwitchActive->setEnabled(has_switch); +} + +void BIOSSettingsWidget::onPIOImagePathBrowseClicked() +{ + const QString path = QDir::toNativeSeparators( + QFileDialog::getOpenFileName(QtUtils::GetRootWidget(this), tr("Select PIO Image"), m_ui.pioImagePath->text())); + if (path.isEmpty()) + return; + + m_ui.pioImagePath->setText(path); +} diff --git a/src/duckstation-qt/biossettingswidget.h b/src/duckstation-qt/biossettingswidget.h index 6b46d04b0..e2ea9304a 100644 --- a/src/duckstation-qt/biossettingswidget.h +++ b/src/duckstation-qt/biossettingswidget.h @@ -30,6 +30,8 @@ public: private Q_SLOTS: void refreshList(); void onFastBootChanged(); + void onPIODeviceTypeChanged(); + void onPIOImagePathBrowseClicked(); private: Ui::BIOSSettingsWidget m_ui; diff --git a/src/duckstation-qt/biossettingswidget.ui b/src/duckstation-qt/biossettingswidget.ui index 1273796c1..2879c39fd 100644 --- a/src/duckstation-qt/biossettingswidget.ui +++ b/src/duckstation-qt/biossettingswidget.ui @@ -6,8 +6,8 @@ 0 0 - 529 - 330 + 732 + 470 @@ -177,6 +177,64 @@ + + + + Parallel Port + + + + + + Device Type: + + + + + + + + + + + + Image Path: + + + + + + + + + + Browse... + + + + + + + + + + + Cartridge Switch On + + + + + + + Allow Image Writes + + + + + + + + @@ -185,7 +243,7 @@ 20 - 40 + 1 diff --git a/src/util/state_wrapper.h b/src/util/state_wrapper.h index 9ac2b86d8..1fde2b5b6 100644 --- a/src/util/state_wrapper.h +++ b/src/util/state_wrapper.h @@ -35,6 +35,7 @@ public: ALWAYS_INLINE bool IsWriting() const { return (m_mode == Mode::Write); } ALWAYS_INLINE u32 GetVersion() const { return m_version; } ALWAYS_INLINE size_t GetPosition() const { return m_pos; } + ALWAYS_INLINE void SetPosition(size_t pos) { m_pos = pos; } /// Overload for integral or floating-point types. Writes bytes as-is. template || std::is_floating_point_v, int> = 0>