PIO: Add basic flash cartridge support

This commit is contained in:
Stenzek 2024-12-13 23:09:36 +10:00
parent 2f6eaa1d43
commit 5687dd22bd
No known key found for this signature in database
18 changed files with 1182 additions and 65 deletions

View File

@ -50,6 +50,7 @@
X(PlatformMisc) \
X(PostProcessing) \
X(ProgressCallback) \
X(PIO) \
X(ReShadeFXShader) \
X(Recompiler) \
X(SDL) \

View File

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

View File

@ -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<TickCount, 3> g_bios_access_time = {};
std::array<TickCount, 3> g_cdrom_access_time = {};
std::array<TickCount, 3> g_spu_access_time = {};
static std::vector<u8> 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<PhysicalMemoryAddress> Bus::SearchMemory(PhysicalMemoryAddress sta
return std::nullopt;
}
void Bus::SetExpansionROM(std::vector<u8> 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<u32>(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<u32>(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<MemoryAccessSize size>
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<MemoryAccessSize size>

View File

@ -5,15 +5,11 @@
#include "types.h"
#include "common/bitfield.h"
#include <array>
#include <bitset>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
class Error;
@ -148,8 +144,6 @@ void* GetFastmemBase(bool isc);
void RemapFastmemViews();
bool CanUseFastmemForAddress(VirtualMemoryAddress address);
void SetExpansionROM(std::vector<u8> data);
extern std::bitset<RAM_8MB_CODE_PAGE_COUNT> g_ram_code_bits;
extern u8* g_ram; // 2MB-8MB RAM
extern u8* g_unprotected_ram; // RAM without page protection, use for debugger access.

View File

@ -69,6 +69,7 @@
</ClCompile>
<ClCompile Include="cpu_pgxp.cpp" />
<ClCompile Include="performance_counters.cpp" />
<ClCompile Include="pio.cpp" />
<ClCompile Include="playstation_mouse.cpp" />
<ClCompile Include="psf_loader.cpp" />
<ClCompile Include="settings.cpp" />
@ -148,6 +149,7 @@
<ClInclude Include="pch.h" />
<ClInclude Include="cpu_pgxp.h" />
<ClInclude Include="performance_counters.h" />
<ClInclude Include="pio.h" />
<ClInclude Include="playstation_mouse.h" />
<ClInclude Include="psf_loader.h" />
<ClInclude Include="save_state_version.h" />

View File

@ -66,6 +66,7 @@
<ClCompile Include="cdrom_subq_replacement.cpp" />
<ClCompile Include="performance_counters.cpp" />
<ClCompile Include="jogcon.cpp" />
<ClCompile Include="pio.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="types.h" />
@ -141,6 +142,7 @@
<ClInclude Include="performance_counters.h" />
<ClInclude Include="system_private.h" />
<ClInclude Include="jogcon.h" />
<ClInclude Include="pio.h" />
</ItemGroup>
<ItemGroup>
<None Include="gpu_sw_rasterizer.inl" />

View File

@ -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<u32>(MemoryAccessSize::Word)] * word_count;
return true;
}
else
{
if (raise_exceptions)

863
src/core/pio.cpp Normal file
View File

@ -0,0 +1,863 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// 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 <limits>
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<u8> m_data;
GlobalTicks m_program_write_timeout = 0;
std::array<u8, 6> 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<u8, 2> 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<u32>(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<GlobalTicks>::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<s64>(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<u32>(std::min<s64>(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<u8, bool, 0, 1> pc_slct;
BitField<u8, bool, 1, 1> pc_pe;
BitField<u8, bool, 2, 1> pc_busy;
BitField<u8, bool, 3, 1> pc_ack;
BitField<u8, bool, 4, 1> sram_select;
BitField<u8, bool, 5, 1> flash_bank;
BitField<u8, bool, 6, 1> sram_bank;
BitField<u8, bool, 7, 1> sram_bank_2;
};
DynamicHeapArray<u8> 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<PIO::Device> CreateDevice(PIODeviceType type);
} // namespace PIO
std::unique_ptr<PIO::Device> 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::Device> PIO::CreateDevice(PIODeviceType type)
{
switch (type)
{
case PIODeviceType::None:
return std::make_unique<NullDevice>();
case PIODeviceType::XplorerCart:
return std::make_unique<XplorerCart>();
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<u32>(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();
}

48
src/core/pio.h Normal file
View File

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "types.h"
#include <memory>
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<PIO::Device> g_pio_device;

View File

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

View File

@ -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<size_t>(PIODeviceType::MaxCount));
static_assert(s_pio_device_type_display_names.size() == static_cast<size_t>(PIODeviceType::MaxCount));
std::optional<PIODeviceType> 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<PIODeviceType>(index);
index++;
}
return std::nullopt;
}
const char* Settings::GetPIODeviceTypeModeName(PIODeviceType type)
{
return s_pio_device_type_names[static_cast<size_t>(type)];
}
const char* Settings::GetPIODeviceTypeModeDisplayName(PIODeviceType type)
{
return Host::TranslateToCString("Settings", s_pio_device_type_display_names[static_cast<size_t>(type)],
"PIODeviceType");
}
std::string EmuFolders::AppRoot;
std::string EmuFolders::DataRoot;
std::string EmuFolders::Bios;

View File

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

View File

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

View File

@ -303,3 +303,10 @@ enum class ForceVideoTimingMode : u8
Count,
};
enum class PIODeviceType : u8
{
None,
XplorerCart,
MaxCount,
};

View File

@ -10,6 +10,7 @@
#include "core/bios.h"
#include "core/settings.h"
#include <QtCore/QDir>
#include <QtWidgets/QFileDialog>
#include <algorithm>
@ -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<int>::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. <strong>This will "
"overwrite your cartridge dump,</strong> you should ensure you have a backup first."));
connect(m_ui.imageNTSCJ, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) {
if (m_dialog->isPerGameSettings() && index == 0)
@ -180,3 +205,31 @@ void BIOSSettingsWidget::setDropDownValue(QComboBox* cb, const std::optional<std
cb->addItem(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);
}

View File

@ -30,6 +30,8 @@ public:
private Q_SLOTS:
void refreshList();
void onFastBootChanged();
void onPIODeviceTypeChanged();
void onPIOImagePathBrowseClicked();
private:
Ui::BIOSSettingsWidget m_ui;

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>529</width>
<height>330</height>
<width>732</width>
<height>470</height>
</rect>
</property>
<layout class="QVBoxLayout" name="mainLayout">
@ -177,6 +177,64 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="pioGroupBox">
<property name="title">
<string>Parallel Port</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Device Type:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="pioDeviceType"/>
</item>
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1,0">
<item>
<widget class="QLabel" name="pioImagePathLabel">
<property name="text">
<string>Image Path:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="pioImagePath"/>
</item>
<item>
<widget class="QPushButton" name="pioImagePathBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QCheckBox" name="pioSwitchActive">
<property name="text">
<string>Cartridge Switch On</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="pioImageWrites">
<property name="text">
<string>Allow Image Writes</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
@ -185,7 +243,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
<height>1</height>
</size>
</property>
</spacer>

View File

@ -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<typename T, std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, int> = 0>