mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-25 23:25:41 +00:00
System: Rewrite EXE override/loading
Relies on POST=7 as a kernel initialization indicator, instead of patching the BIOS. Fixes EXE loading with OpenBIOS and PS2 BIOS, and fast boot getting baked into save states.
This commit is contained in:
parent
7b99fcbbf3
commit
6fe0c986fa
|
@ -278,7 +278,7 @@ std::string Achievements::GetGameHash(CDImage* image)
|
||||||
BIOS::PSEXEHeader header = {};
|
BIOS::PSEXEHeader header = {};
|
||||||
if (executable_data.size() >= sizeof(header))
|
if (executable_data.size() >= sizeof(header))
|
||||||
std::memcpy(&header, executable_data.data(), sizeof(header));
|
std::memcpy(&header, executable_data.data(), sizeof(header));
|
||||||
if (!BIOS::IsValidPSExeHeader(header, static_cast<u32>(executable_data.size())))
|
if (!BIOS::IsValidPSExeHeader(header, executable_data.size()))
|
||||||
{
|
{
|
||||||
ERROR_LOG("PS-EXE header is invalid in '{}' ({} bytes)", executable_name, executable_data.size());
|
ERROR_LOG("PS-EXE header is invalid in '{}' ({} bytes)", executable_name, executable_data.size());
|
||||||
return {};
|
return {};
|
||||||
|
|
|
@ -19,6 +19,7 @@ Log_SetChannel(BIOS);
|
||||||
|
|
||||||
namespace BIOS {
|
namespace BIOS {
|
||||||
static const ImageInfo* GetInfoForHash(const std::span<u8> image, const ImageInfo::Hash& hash);
|
static const ImageInfo* GetInfoForHash(const std::span<u8> image, const ImageInfo::Hash& hash);
|
||||||
|
static void PatchBIOS(u8* image, u32 image_size, u32 address, u32 value, u32 mask = UINT32_C(0xFFFFFFFF));
|
||||||
|
|
||||||
static constexpr ImageInfo::Hash MakeHashFromString(const char str[])
|
static constexpr ImageInfo::Hash MakeHashFromString(const char str[])
|
||||||
{
|
{
|
||||||
|
@ -255,54 +256,16 @@ bool BIOS::PatchBIOSFastBoot(u8* image, u32 image_size)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BIOS::PatchBIOSForEXE(u8* image, u32 image_size, u32 r_pc, u32 r_gp, u32 r_sp, u32 r_fp)
|
bool BIOS::IsValidPSExeHeader(const PSEXEHeader& header, size_t file_size)
|
||||||
{
|
|
||||||
#define PATCH(offset, value) PatchBIOS(image, image_size, (offset), (value))
|
|
||||||
|
|
||||||
// pc has to be done first because we can't load it in the delay slot
|
|
||||||
PATCH(0xBFC06FF0, UINT32_C(0x3C080000) | r_pc >> 16); // lui $t0, (r_pc >> 16)
|
|
||||||
PATCH(0xBFC06FF4, UINT32_C(0x35080000) | (r_pc & UINT32_C(0xFFFF))); // ori $t0, $t0, (r_pc & 0xFFFF)
|
|
||||||
PATCH(0xBFC06FF8, UINT32_C(0x3C1C0000) | r_gp >> 16); // lui $gp, (r_gp >> 16)
|
|
||||||
PATCH(0xBFC06FFC, UINT32_C(0x379C0000) | (r_gp & UINT32_C(0xFFFF))); // ori $gp, $gp, (r_gp & 0xFFFF)
|
|
||||||
|
|
||||||
if (r_sp != 0)
|
|
||||||
{
|
|
||||||
PATCH(0xBFC07000, UINT32_C(0x3C1D0000) | r_sp >> 16); // lui $sp, (r_sp >> 16)
|
|
||||||
PATCH(0xBFC07004, UINT32_C(0x37BD0000) | (r_sp & UINT32_C(0xFFFF))); // ori $sp, $sp, (r_sp & 0xFFFF)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PATCH(0xBFC07000, UINT32_C(0x00000000)); // nop
|
|
||||||
PATCH(0xBFC07004, UINT32_C(0x00000000)); // nop
|
|
||||||
}
|
|
||||||
if (r_fp != 0)
|
|
||||||
{
|
|
||||||
PATCH(0xBFC07008, UINT32_C(0x3C1E0000) | r_fp >> 16); // lui $fp, (r_fp >> 16)
|
|
||||||
PATCH(0xBFC0700C, UINT32_C(0x01000008)); // jr $t0
|
|
||||||
PATCH(0xBFC07010, UINT32_C(0x37DE0000) | (r_fp & UINT32_C(0xFFFF))); // ori $fp, $fp, (r_fp & 0xFFFF)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PATCH(0xBFC07008, UINT32_C(0x00000000)); // nop
|
|
||||||
PATCH(0xBFC0700C, UINT32_C(0x01000008)); // jr $t0
|
|
||||||
PATCH(0xBFC07010, UINT32_C(0x00000000)); // nop
|
|
||||||
}
|
|
||||||
|
|
||||||
#undef PATCH
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BIOS::IsValidPSExeHeader(const PSEXEHeader& header, u32 file_size)
|
|
||||||
{
|
{
|
||||||
static constexpr char expected_id[] = {'P', 'S', '-', 'X', ' ', 'E', 'X', 'E'};
|
static constexpr char expected_id[] = {'P', 'S', '-', 'X', ' ', 'E', 'X', 'E'};
|
||||||
if (std::memcmp(header.id, expected_id, sizeof(expected_id)) != 0)
|
if (file_size < sizeof(expected_id) || std::memcmp(header.id, expected_id, sizeof(expected_id)) != 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if ((header.file_size + sizeof(PSEXEHeader)) > file_size)
|
if ((header.file_size + sizeof(PSEXEHeader)) > file_size)
|
||||||
{
|
{
|
||||||
WARNING_LOG("Incorrect file size in PS-EXE header: {} bytes should not be greater than {} bytes", header.file_size,
|
WARNING_LOG("Incorrect file size in PS-EXE header: {} bytes should not be greater than {} bytes", header.file_size,
|
||||||
static_cast<unsigned>(file_size - sizeof(PSEXEHeader)));
|
file_size - sizeof(PSEXEHeader));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -70,12 +70,9 @@ std::optional<Image> LoadImageFromFile(const char* filename, Error* error);
|
||||||
|
|
||||||
bool IsValidBIOSForRegion(ConsoleRegion console_region, ConsoleRegion bios_region);
|
bool IsValidBIOSForRegion(ConsoleRegion console_region, ConsoleRegion bios_region);
|
||||||
|
|
||||||
void PatchBIOS(u8* image, u32 image_size, u32 address, u32 value, u32 mask = UINT32_C(0xFFFFFFFF));
|
|
||||||
|
|
||||||
bool PatchBIOSFastBoot(u8* image, u32 image_size);
|
bool PatchBIOSFastBoot(u8* image, u32 image_size);
|
||||||
bool PatchBIOSForEXE(u8* image, u32 image_size, u32 r_pc, u32 r_gp, u32 r_sp, u32 r_fp);
|
|
||||||
|
|
||||||
bool IsValidPSExeHeader(const PSEXEHeader& header, u32 file_size);
|
bool IsValidPSExeHeader(const PSEXEHeader& header, size_t file_size);
|
||||||
DiscRegion GetPSExeDiscRegion(const PSEXEHeader& header);
|
DiscRegion GetPSExeDiscRegion(const PSEXEHeader& header);
|
||||||
|
|
||||||
/// Loads the BIOS image for the specified region.
|
/// Loads the BIOS image for the specified region.
|
||||||
|
|
200
src/core/bus.cpp
200
src/core/bus.cpp
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||||
|
|
||||||
#include "bus.h"
|
#include "bus.h"
|
||||||
|
#include "bios.h"
|
||||||
#include "cdrom.h"
|
#include "cdrom.h"
|
||||||
#include "cpu_code_cache.h"
|
#include "cpu_code_cache.h"
|
||||||
#include "cpu_core.h"
|
#include "cpu_core.h"
|
||||||
|
@ -13,6 +14,7 @@
|
||||||
#include "interrupt_controller.h"
|
#include "interrupt_controller.h"
|
||||||
#include "mdec.h"
|
#include "mdec.h"
|
||||||
#include "pad.h"
|
#include "pad.h"
|
||||||
|
#include "psf_loader.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "sio.h"
|
#include "sio.h"
|
||||||
#include "spu.h"
|
#include "spu.h"
|
||||||
|
@ -20,14 +22,17 @@
|
||||||
#include "timers.h"
|
#include "timers.h"
|
||||||
#include "timing_event.h"
|
#include "timing_event.h"
|
||||||
|
|
||||||
|
#include "util/cd_image.h"
|
||||||
#include "util/state_wrapper.h"
|
#include "util/state_wrapper.h"
|
||||||
|
|
||||||
#include "common/align.h"
|
#include "common/align.h"
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/error.h"
|
#include "common/error.h"
|
||||||
|
#include "common/file_system.h"
|
||||||
#include "common/intrin.h"
|
#include "common/intrin.h"
|
||||||
#include "common/log.h"
|
#include "common/log.h"
|
||||||
#include "common/memmap.h"
|
#include "common/memmap.h"
|
||||||
|
#include "common/path.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
|
@ -146,6 +151,8 @@ static std::vector<std::pair<u8*, size_t>> s_fastmem_ram_views;
|
||||||
|
|
||||||
static u8** s_fastmem_lut = nullptr;
|
static u8** s_fastmem_lut = nullptr;
|
||||||
|
|
||||||
|
static bool s_kernel_initialize_hook_run = false;
|
||||||
|
|
||||||
static void SetRAMSize(bool enable_8mb_ram);
|
static void SetRAMSize(bool enable_8mb_ram);
|
||||||
|
|
||||||
static std::tuple<TickCount, TickCount, TickCount> CalculateMemoryTiming(MEMDELAY mem_delay, COMDELAY common_delay);
|
static std::tuple<TickCount, TickCount, TickCount> CalculateMemoryTiming(MEMDELAY mem_delay, COMDELAY common_delay);
|
||||||
|
@ -155,6 +162,9 @@ static u8* GetLUTFastmemPointer(u32 address, u8* ram_ptr);
|
||||||
|
|
||||||
static void SetRAMPageWritable(u32 page_index, bool writable);
|
static void SetRAMPageWritable(u32 page_index, bool writable);
|
||||||
|
|
||||||
|
static void KernelInitializedHook();
|
||||||
|
static bool SideloadEXE(const std::string& path, Error* error);
|
||||||
|
|
||||||
static void SetHandlers();
|
static void SetHandlers();
|
||||||
static void UpdateMappedRAMSize();
|
static void UpdateMappedRAMSize();
|
||||||
|
|
||||||
|
@ -348,6 +358,7 @@ void Bus::Reset()
|
||||||
s_MEMCTRL.exp2_delay_size.bits = 0x00070777;
|
s_MEMCTRL.exp2_delay_size.bits = 0x00070777;
|
||||||
s_MEMCTRL.common_delay.bits = 0x00031125;
|
s_MEMCTRL.common_delay.bits = 0x00031125;
|
||||||
g_ram_code_bits = {};
|
g_ram_code_bits = {};
|
||||||
|
s_kernel_initialize_hook_run = false;
|
||||||
RecalculateMemoryTimings();
|
RecalculateMemoryTimings();
|
||||||
|
|
||||||
// Avoid remapping if unchanged.
|
// Avoid remapping if unchanged.
|
||||||
|
@ -358,35 +369,6 @@ void Bus::Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bus::AddTTYCharacter(char ch)
|
|
||||||
{
|
|
||||||
if (ch == '\r')
|
|
||||||
{
|
|
||||||
}
|
|
||||||
else if (ch == '\n')
|
|
||||||
{
|
|
||||||
if (!s_tty_line_buffer.empty())
|
|
||||||
{
|
|
||||||
Log::FastWrite("TTY", "", LOGLEVEL_INFO, "\033[1;34m{}\033[0m", s_tty_line_buffer);
|
|
||||||
#ifdef _DEBUG
|
|
||||||
if (CPU::IsTraceEnabled())
|
|
||||||
CPU::WriteToExecutionLog("TTY: %s\n", s_tty_line_buffer.c_str());
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
s_tty_line_buffer.clear();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
s_tty_line_buffer += ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Bus::AddTTYString(std::string_view str)
|
|
||||||
{
|
|
||||||
for (char ch : str)
|
|
||||||
AddTTYCharacter(ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Bus::DoState(StateWrapper& sw)
|
bool Bus::DoState(StateWrapper& sw)
|
||||||
{
|
{
|
||||||
u32 ram_size = g_ram_size;
|
u32 ram_size = g_ram_size;
|
||||||
|
@ -420,12 +402,10 @@ bool Bus::DoState(StateWrapper& sw)
|
||||||
UpdateMappedRAMSize();
|
UpdateMappedRAMSize();
|
||||||
|
|
||||||
sw.Do(&s_tty_line_buffer);
|
sw.Do(&s_tty_line_buffer);
|
||||||
return !sw.HasError();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Bus::SetExpansionROM(std::vector<u8> data)
|
sw.DoEx(&s_kernel_initialize_hook_run, 68, true);
|
||||||
{
|
|
||||||
s_exp1_rom = std::move(data);
|
return !sw.HasError();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::tuple<TickCount, TickCount, TickCount> Bus::CalculateMemoryTiming(MEMDELAY mem_delay, COMDELAY common_delay)
|
std::tuple<TickCount, TickCount, TickCount> Bus::CalculateMemoryTiming(MEMDELAY mem_delay, COMDELAY common_delay)
|
||||||
|
@ -863,6 +843,146 @@ std::optional<PhysicalMemoryAddress> Bus::SearchMemory(PhysicalMemoryAddress sta
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Bus::SetExpansionROM(std::vector<u8> data)
|
||||||
|
{
|
||||||
|
s_exp1_rom = std::move(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bus::AddTTYCharacter(char ch)
|
||||||
|
{
|
||||||
|
if (ch == '\r')
|
||||||
|
{
|
||||||
|
}
|
||||||
|
else if (ch == '\n')
|
||||||
|
{
|
||||||
|
if (!s_tty_line_buffer.empty())
|
||||||
|
{
|
||||||
|
Log::FastWrite("TTY", "", LOGLEVEL_INFO, "\033[1;34m{}\033[0m", s_tty_line_buffer);
|
||||||
|
#ifdef _DEBUG
|
||||||
|
if (CPU::IsTraceEnabled())
|
||||||
|
CPU::WriteToExecutionLog("TTY: %s\n", s_tty_line_buffer.c_str());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
s_tty_line_buffer.clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
s_tty_line_buffer += ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bus::AddTTYString(std::string_view str)
|
||||||
|
{
|
||||||
|
for (char ch : str)
|
||||||
|
AddTTYCharacter(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Bus::InjectExecutable(std::span<const u8> buffer, bool set_pc, Error* error)
|
||||||
|
{
|
||||||
|
BIOS::PSEXEHeader header;
|
||||||
|
if (buffer.size() < sizeof(header))
|
||||||
|
{
|
||||||
|
Error::SetStringView(error, "Executable does not contain a header.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(&header, buffer.data(), sizeof(header));
|
||||||
|
if (!BIOS::IsValidPSExeHeader(header, buffer.size()))
|
||||||
|
{
|
||||||
|
Error::SetStringView(error, "Executable does not contain a valid header.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.memfill_size > 0)
|
||||||
|
{
|
||||||
|
const u32 words_to_write = header.memfill_size / 4;
|
||||||
|
u32 address = header.memfill_start & ~UINT32_C(3);
|
||||||
|
for (u32 i = 0; i < words_to_write; i++)
|
||||||
|
{
|
||||||
|
CPU::SafeWriteMemoryWord(address, 0);
|
||||||
|
address += sizeof(u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const u32 data_load_size =
|
||||||
|
std::min(static_cast<u32>(static_cast<u32>(buffer.size() - sizeof(BIOS::PSEXEHeader))), header.file_size);
|
||||||
|
if (data_load_size > 0)
|
||||||
|
{
|
||||||
|
if (!CPU::SafeWriteMemoryBytes(header.load_address, &buffer[sizeof(header)], data_load_size))
|
||||||
|
{
|
||||||
|
Error::SetStringFmt(error, "Failed to upload {} bytes to memory at address 0x{:08X}.", data_load_size,
|
||||||
|
header.load_address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// patch the BIOS to jump to the executable directly
|
||||||
|
if (set_pc)
|
||||||
|
{
|
||||||
|
const u32 r_pc = header.initial_pc;
|
||||||
|
CPU::g_state.regs.gp = header.initial_gp;
|
||||||
|
CPU::g_state.regs.sp = header.initial_sp_base + header.initial_sp_offset;
|
||||||
|
CPU::g_state.regs.fp = header.initial_sp_base + header.initial_sp_offset;
|
||||||
|
CPU::SetPC(r_pc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bus::KernelInitializedHook()
|
||||||
|
{
|
||||||
|
if (s_kernel_initialize_hook_run)
|
||||||
|
return;
|
||||||
|
|
||||||
|
INFO_LOG("Kernel initialized.");
|
||||||
|
s_kernel_initialize_hook_run = true;
|
||||||
|
|
||||||
|
const System::BootMode boot_mode = System::GetBootMode();
|
||||||
|
if (boot_mode == System::BootMode::BootEXE || boot_mode == System::BootMode::BootPSF)
|
||||||
|
{
|
||||||
|
Error error;
|
||||||
|
if (((boot_mode == System::BootMode::BootEXE) ? SideloadEXE(System::GetExeOverride(), &error) :
|
||||||
|
PSFLoader::Load(System::GetExeOverride(), &error)))
|
||||||
|
{
|
||||||
|
// Clear all state, since we're blatently overwriting memory.
|
||||||
|
CPU::CodeCache::Reset();
|
||||||
|
CPU::ClearICache();
|
||||||
|
|
||||||
|
// Stop executing the current block and shell init, and jump straight to the new code.
|
||||||
|
DebugAssert(!TimingEvents::IsRunningEvents());
|
||||||
|
CPU::ExitExecution();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Shut down system on load failure.
|
||||||
|
Host::ReportErrorAsync("EXE/PSF Load Failed", error.GetDescription());
|
||||||
|
System::ShutdownSystem(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Bus::SideloadEXE(const std::string& path, Error* error)
|
||||||
|
{
|
||||||
|
// look for a libps.exe next to the exe, if it exists, load it
|
||||||
|
bool okay = true;
|
||||||
|
if (const std::string libps_path = Path::BuildRelativePath(path, "libps.exe");
|
||||||
|
FileSystem::FileExists(libps_path.c_str()))
|
||||||
|
{
|
||||||
|
const std::optional<std::vector<u8>> exe_data = FileSystem::ReadBinaryFile(libps_path.c_str(), error);
|
||||||
|
okay = (exe_data.has_value() && InjectExecutable(exe_data.value(), false, error));
|
||||||
|
if (!okay)
|
||||||
|
Error::AddPrefix(error, "Failed to load libps.exe: ");
|
||||||
|
}
|
||||||
|
if (okay)
|
||||||
|
{
|
||||||
|
const std::optional<std::vector<u8>> exe_data = FileSystem::ReadBinaryFile(System::GetExeOverride().c_str(), error);
|
||||||
|
okay = (exe_data.has_value() && InjectExecutable(exe_data.value(), true, error));
|
||||||
|
if (!okay)
|
||||||
|
Error::AddPrefixFmt(error, "Failed to load {}: ", Path::GetFileName(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
return okay;
|
||||||
|
}
|
||||||
|
|
||||||
#define BUS_CYCLES(n) CPU::g_state.pending_ticks += n
|
#define BUS_CYCLES(n) CPU::g_state.pending_ticks += n
|
||||||
|
|
||||||
// TODO: Move handlers to own files for better inlining.
|
// TODO: Move handlers to own files for better inlining.
|
||||||
|
@ -1192,7 +1312,10 @@ void Bus::EXP2WriteHandler(VirtualMemoryAddress address, u32 value)
|
||||||
}
|
}
|
||||||
else if (offset == 0x41 || offset == 0x42)
|
else if (offset == 0x41 || offset == 0x42)
|
||||||
{
|
{
|
||||||
DEV_LOG("BIOS POST status: {:02X}", value & UINT32_C(0x0F));
|
const u32 post_code = value & UINT32_C(0x0F);
|
||||||
|
DEV_LOG("BIOS POST status: {:02X}", post_code);
|
||||||
|
if (post_code == 0x07)
|
||||||
|
KernelInitializedHook();
|
||||||
}
|
}
|
||||||
else if (offset == 0x70)
|
else if (offset == 0x70)
|
||||||
{
|
{
|
||||||
|
@ -1233,7 +1356,12 @@ void Bus::EXP3WriteHandler(VirtualMemoryAddress address, u32 value)
|
||||||
{
|
{
|
||||||
const u32 offset = address & EXP3_MASK;
|
const u32 offset = address & EXP3_MASK;
|
||||||
if (offset == 0)
|
if (offset == 0)
|
||||||
WARNING_LOG("BIOS POST3 status: {:02X}", value & UINT32_C(0x0F));
|
{
|
||||||
|
const u32 post_code = value & UINT32_C(0x0F);
|
||||||
|
WARNING_LOG("BIOS POST3 status: {:02X}", post_code);
|
||||||
|
if (post_code == 0x07)
|
||||||
|
KernelInitializedHook();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <bitset>
|
#include <bitset>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
@ -218,4 +219,7 @@ std::optional<PhysicalMemoryAddress> SearchMemory(PhysicalMemoryAddress start_ad
|
||||||
void AddTTYCharacter(char ch);
|
void AddTTYCharacter(char ch);
|
||||||
void AddTTYString(std::string_view str);
|
void AddTTYString(std::string_view str);
|
||||||
|
|
||||||
|
/// Injects a PS-EXE into memory at its specified load location. If set_pc is set, execution will be redirected.
|
||||||
|
bool InjectExecutable(std::span<const u8> buffer, bool set_pc, Error* error);
|
||||||
|
|
||||||
} // namespace Bus
|
} // namespace Bus
|
||||||
|
|
|
@ -28,7 +28,6 @@ Log_SetChannel(CPU::Core);
|
||||||
|
|
||||||
namespace CPU {
|
namespace CPU {
|
||||||
static bool ShouldUseInterpreter();
|
static bool ShouldUseInterpreter();
|
||||||
static void SetPC(u32 new_pc);
|
|
||||||
static void UpdateLoadDelay();
|
static void UpdateLoadDelay();
|
||||||
static void Branch(u32 target);
|
static void Branch(u32 target);
|
||||||
static void FlushLoadDelay();
|
static void FlushLoadDelay();
|
||||||
|
@ -306,7 +305,7 @@ ALWAYS_INLINE_RELEASE bool CPU::ShouldUseInterpreter()
|
||||||
return (g_settings.cpu_execution_mode == CPUExecutionMode::Interpreter || g_state.using_debug_dispatcher);
|
return (g_settings.cpu_execution_mode == CPUExecutionMode::Interpreter || g_state.using_debug_dispatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
ALWAYS_INLINE_RELEASE void CPU::SetPC(u32 new_pc)
|
void CPU::SetPC(u32 new_pc)
|
||||||
{
|
{
|
||||||
DebugAssert(Common::IsAlignedPow2(new_pc, 4));
|
DebugAssert(Common::IsAlignedPow2(new_pc, 4));
|
||||||
g_state.npc = new_pc;
|
g_state.npc = new_pc;
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
namespace CPU {
|
namespace CPU {
|
||||||
|
|
||||||
|
void SetPC(u32 new_pc);
|
||||||
|
|
||||||
// exceptions
|
// exceptions
|
||||||
void RaiseException(Exception excode);
|
void RaiseException(Exception excode);
|
||||||
void RaiseException(u32 CAUSE_bits, u32 EPC);
|
void RaiseException(u32 CAUSE_bits, u32 EPC);
|
||||||
|
|
|
@ -202,7 +202,7 @@ bool GameList::GetPsfListEntry(const std::string& path, Entry* entry)
|
||||||
{
|
{
|
||||||
// we don't need to walk the library chain here - the top file is enough
|
// we don't need to walk the library chain here - the top file is enough
|
||||||
PSFLoader::File file;
|
PSFLoader::File file;
|
||||||
if (!file.Load(path.c_str()))
|
if (!file.Load(path.c_str(), nullptr))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
entry->serial.clear();
|
entry->serial.clear();
|
||||||
|
|
|
@ -1,21 +1,29 @@
|
||||||
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
|
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||||
|
|
||||||
#include "psf_loader.h"
|
#include "psf_loader.h"
|
||||||
#include "bios.h"
|
#include "bios.h"
|
||||||
|
#include "bus.h"
|
||||||
|
#include "system.h"
|
||||||
|
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
|
#include "common/error.h"
|
||||||
#include "common/file_system.h"
|
#include "common/file_system.h"
|
||||||
#include "common/log.h"
|
#include "common/log.h"
|
||||||
#include "common/path.h"
|
#include "common/path.h"
|
||||||
#include "system.h"
|
|
||||||
#include "zlib.h"
|
#include "zlib.h"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
Log_SetChannel(PSFLoader);
|
Log_SetChannel(PSFLoader);
|
||||||
|
|
||||||
namespace PSFLoader {
|
namespace PSFLoader {
|
||||||
|
static bool LoadLibraryPSF(const std::string& path, bool use_pc_sp, Error* error, u32 depth = 0);
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<std::string> File::GetTagString(const char* tag_name) const
|
std::optional<std::string> PSFLoader::File::GetTagString(const char* tag_name) const
|
||||||
{
|
{
|
||||||
auto it = m_tags.find(tag_name);
|
auto it = m_tags.find(tag_name);
|
||||||
if (it == m_tags.end())
|
if (it == m_tags.end())
|
||||||
|
@ -24,7 +32,7 @@ std::optional<std::string> File::GetTagString(const char* tag_name) const
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<int> File::GetTagInt(const char* tag_name) const
|
std::optional<int> PSFLoader::File::GetTagInt(const char* tag_name) const
|
||||||
{
|
{
|
||||||
auto it = m_tags.find(tag_name);
|
auto it = m_tags.find(tag_name);
|
||||||
if (it == m_tags.end())
|
if (it == m_tags.end())
|
||||||
|
@ -33,7 +41,7 @@ std::optional<int> File::GetTagInt(const char* tag_name) const
|
||||||
return std::atoi(it->second.c_str());
|
return std::atoi(it->second.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<float> File::GetTagFloat(const char* tag_name) const
|
std::optional<float> PSFLoader::File::GetTagFloat(const char* tag_name) const
|
||||||
{
|
{
|
||||||
auto it = m_tags.find(tag_name);
|
auto it = m_tags.find(tag_name);
|
||||||
if (it == m_tags.end())
|
if (it == m_tags.end())
|
||||||
|
@ -42,7 +50,7 @@ std::optional<float> File::GetTagFloat(const char* tag_name) const
|
||||||
return static_cast<float>(std::atof(it->second.c_str()));
|
return static_cast<float>(std::atof(it->second.c_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string File::GetTagString(const char* tag_name, const char* default_value) const
|
std::string PSFLoader::File::GetTagString(const char* tag_name, const char* default_value) const
|
||||||
{
|
{
|
||||||
std::optional<std::string> value(GetTagString(tag_name));
|
std::optional<std::string> value(GetTagString(tag_name));
|
||||||
if (value.has_value())
|
if (value.has_value())
|
||||||
|
@ -51,24 +59,21 @@ std::string File::GetTagString(const char* tag_name, const char* default_value)
|
||||||
return default_value;
|
return default_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
int File::GetTagInt(const char* tag_name, int default_value) const
|
int PSFLoader::File::GetTagInt(const char* tag_name, int default_value) const
|
||||||
{
|
{
|
||||||
return GetTagInt(tag_name).value_or(default_value);
|
return GetTagInt(tag_name).value_or(default_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
float File::GetTagFloat(const char* tag_name, float default_value) const
|
float PSFLoader::File::GetTagFloat(const char* tag_name, float default_value) const
|
||||||
{
|
{
|
||||||
return GetTagFloat(tag_name).value_or(default_value);
|
return GetTagFloat(tag_name).value_or(default_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool File::Load(const char* path)
|
bool PSFLoader::File::Load(const char* path, Error* error)
|
||||||
{
|
{
|
||||||
std::optional<std::vector<u8>> file_data(FileSystem::ReadBinaryFile(path));
|
std::optional<std::vector<u8>> file_data(FileSystem::ReadBinaryFile(path, error));
|
||||||
if (!file_data.has_value() || file_data->empty())
|
if (!file_data.has_value() || file_data->empty())
|
||||||
{
|
|
||||||
ERROR_LOG("Failed to open/read PSF file '{}'", path);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
const u8* file_pointer = file_data->data();
|
const u8* file_pointer = file_data->data();
|
||||||
const u8* file_pointer_end = file_data->data() + file_data->size();
|
const u8* file_pointer_end = file_data->data() + file_data->size();
|
||||||
|
@ -81,7 +86,7 @@ bool File::Load(const char* path)
|
||||||
header.compressed_program_size == 0 ||
|
header.compressed_program_size == 0 ||
|
||||||
(sizeof(header) + header.reserved_area_size + header.compressed_program_size) > file_size)
|
(sizeof(header) + header.reserved_area_size + header.compressed_program_size) > file_size)
|
||||||
{
|
{
|
||||||
ERROR_LOG("Invalid or incompatible header in PSF '{}'", path);
|
Error::SetStringView(error, "Invalid or incompatible PSF header.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +103,7 @@ bool File::Load(const char* path)
|
||||||
int err = inflateInit(&strm);
|
int err = inflateInit(&strm);
|
||||||
if (err != Z_OK)
|
if (err != Z_OK)
|
||||||
{
|
{
|
||||||
ERROR_LOG("inflateInit() failed: {}", err);
|
Error::SetStringFmt(error, "inflateInit() failed: {}", err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +111,7 @@ bool File::Load(const char* path)
|
||||||
err = inflate(&strm, Z_NO_FLUSH);
|
err = inflate(&strm, Z_NO_FLUSH);
|
||||||
if (err != Z_STREAM_END)
|
if (err != Z_STREAM_END)
|
||||||
{
|
{
|
||||||
ERROR_LOG("inflate() failed: {}", err);
|
Error::SetStringFmt(error, "inflate() failed: {}", err);
|
||||||
inflateEnd(&strm);
|
inflateEnd(&strm);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -166,19 +171,20 @@ bool File::Load(const char* path)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool LoadLibraryPSF(const char* path, bool use_pc_sp, u32 depth = 0)
|
bool PSFLoader::LoadLibraryPSF(const std::string& path, bool use_pc_sp, Error* error, u32 depth)
|
||||||
{
|
{
|
||||||
// don't recurse past 10 levels just in case of broken files
|
// don't recurse past 10 levels just in case of broken files
|
||||||
if (depth >= 10)
|
if (depth >= 10)
|
||||||
{
|
{
|
||||||
ERROR_LOG("Recursion depth exceeded when loading PSF '{}'", path);
|
Error::SetStringFmt(error, "Recursion depth exceeded when loading PSF '{}'", Path::GetFileName(path));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
File file;
|
File file;
|
||||||
if (!file.Load(path))
|
if (!file.Load(path.c_str(), error))
|
||||||
{
|
{
|
||||||
ERROR_LOG("Failed to load main PSF '{}'", path);
|
Error::AddPrefixFmt(error, "Failed to load {} PSF '{}': ", (depth == 0) ? "main" : "parent",
|
||||||
|
Path::GetFileName(path));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,16 +192,13 @@ static bool LoadLibraryPSF(const char* path, bool use_pc_sp, u32 depth = 0)
|
||||||
std::optional<std::string> lib_name(file.GetTagString("_lib"));
|
std::optional<std::string> lib_name(file.GetTagString("_lib"));
|
||||||
if (lib_name.has_value())
|
if (lib_name.has_value())
|
||||||
{
|
{
|
||||||
const std::string lib_path(Path::BuildRelativePath(path, lib_name.value()));
|
const std::string lib_path = Path::BuildRelativePath(path, lib_name.value());
|
||||||
INFO_LOG("Loading main parent PSF '{}'", lib_path);
|
INFO_LOG("Loading parent PSF '{}'", Path::GetFileName(lib_path));
|
||||||
|
|
||||||
// We should use the initial SP/PC from the **first** parent lib.
|
// We should use the initial SP/PC from the **first** parent lib.
|
||||||
const bool lib_use_pc_sp = (depth == 0);
|
const bool lib_use_pc_sp = (depth == 0);
|
||||||
if (!LoadLibraryPSF(lib_path.c_str(), lib_use_pc_sp, depth + 1))
|
if (!LoadLibraryPSF(lib_path.c_str(), lib_use_pc_sp, error, depth + 1))
|
||||||
{
|
|
||||||
ERROR_LOG("Failed to load main parent PSF '{}'", lib_path);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
// Don't apply the PC/SP from the minipsf file.
|
// Don't apply the PC/SP from the minipsf file.
|
||||||
if (lib_use_pc_sp)
|
if (lib_use_pc_sp)
|
||||||
|
@ -203,10 +206,10 @@ static bool LoadLibraryPSF(const char* path, bool use_pc_sp, u32 depth = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply the main psf
|
// apply the main psf
|
||||||
if (!System::InjectEXEFromBuffer(file.GetProgramData().data(), static_cast<u32>(file.GetProgramData().size()),
|
if (!Bus::InjectExecutable(file.GetProgramData(), use_pc_sp, error))
|
||||||
use_pc_sp))
|
|
||||||
{
|
{
|
||||||
ERROR_LOG("Failed to parse EXE from PSF '{}'", path);
|
Error::AddPrefixFmt(error, "Failed to inject {} PSF '{}': ", (depth == 0) ? "main" : "parent",
|
||||||
|
Path::GetFileName(path));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,22 +221,17 @@ static bool LoadLibraryPSF(const char* path, bool use_pc_sp, u32 depth = 0)
|
||||||
if (!lib_name.has_value())
|
if (!lib_name.has_value())
|
||||||
break;
|
break;
|
||||||
|
|
||||||
const std::string lib_path(Path::BuildRelativePath(path, lib_name.value()));
|
const std::string lib_path = Path::BuildRelativePath(path, lib_name.value());
|
||||||
INFO_LOG("Loading parent PSF '{}'", lib_path);
|
INFO_LOG("Loading parent PSF '{}'", Path::GetFileName(lib_path));
|
||||||
if (!LoadLibraryPSF(lib_path.c_str(), false, depth + 1))
|
if (!LoadLibraryPSF(lib_path.c_str(), false, error, depth + 1))
|
||||||
{
|
|
||||||
ERROR_LOG("Failed to load parent PSF '{}'", lib_path);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Load(const char* path)
|
bool PSFLoader::Load(const std::string& path, Error* error)
|
||||||
{
|
{
|
||||||
INFO_LOG("Loading PSF file from '{}'", path);
|
INFO_LOG("Loading PSF file from '{}'", path);
|
||||||
return LoadLibraryPSF(path, true);
|
return LoadLibraryPSF(path, true, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace PSFLoader
|
|
|
@ -1,14 +1,18 @@
|
||||||
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
|
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
class Error;
|
||||||
|
|
||||||
namespace PSFLoader {
|
namespace PSFLoader {
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
#pragma pack(push, 1)
|
||||||
|
@ -40,7 +44,7 @@ public:
|
||||||
int GetTagInt(const char* tag_name, int default_value) const;
|
int GetTagInt(const char* tag_name, int default_value) const;
|
||||||
float GetTagFloat(const char* tag_name, float default_value) const;
|
float GetTagFloat(const char* tag_name, float default_value) const;
|
||||||
|
|
||||||
bool Load(const char* path);
|
bool Load(const char* path, Error* error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum : u32
|
enum : u32
|
||||||
|
@ -53,6 +57,6 @@ private:
|
||||||
DiscRegion m_region = DiscRegion::Other;
|
DiscRegion m_region = DiscRegion::Other;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool Load(const char* path);
|
bool Load(const std::string& path, Error* error);
|
||||||
|
|
||||||
} // namespace PSFLoader
|
} // namespace PSFLoader
|
|
@ -5,7 +5,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 = 67;
|
static constexpr u32 SAVE_STATE_VERSION = 68;
|
||||||
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);
|
||||||
|
|
|
@ -108,8 +108,6 @@ static std::optional<ExtendedSaveStateInfo> InternalGetExtendedSaveStateInfo(Byt
|
||||||
|
|
||||||
static void LoadInputBindings(SettingsInterface& si, std::unique_lock<std::mutex>& lock);
|
static void LoadInputBindings(SettingsInterface& si, std::unique_lock<std::mutex>& lock);
|
||||||
|
|
||||||
static bool LoadEXE(const char* filename);
|
|
||||||
|
|
||||||
static std::string GetExecutableNameForImage(IsoReader& iso, bool strip_subdirectories);
|
static std::string GetExecutableNameForImage(IsoReader& iso, bool strip_subdirectories);
|
||||||
static bool ReadExecutableFromImage(IsoReader& iso, std::string* out_executable_name,
|
static bool ReadExecutableFromImage(IsoReader& iso, std::string* out_executable_name,
|
||||||
std::vector<u8>* out_executable_data);
|
std::vector<u8>* out_executable_data);
|
||||||
|
@ -117,6 +115,7 @@ static GameHash GetGameHashFromBuffer(std::string_view exe_name, std::span<const
|
||||||
const IsoReader::ISOPrimaryVolumeDescriptor& iso_pvd, u32 track_1_length);
|
const IsoReader::ISOPrimaryVolumeDescriptor& iso_pvd, u32 track_1_length);
|
||||||
|
|
||||||
static bool LoadBIOS(Error* error);
|
static bool LoadBIOS(Error* error);
|
||||||
|
static void ResetBootMode();
|
||||||
static void InternalReset();
|
static void InternalReset();
|
||||||
static void ClearRunningGame();
|
static void ClearRunningGame();
|
||||||
static void DestroySystem();
|
static void DestroySystem();
|
||||||
|
@ -183,10 +182,11 @@ static BIOS::ImageInfo::Hash s_bios_hash = {};
|
||||||
static std::string s_running_game_path;
|
static std::string s_running_game_path;
|
||||||
static std::string s_running_game_serial;
|
static std::string s_running_game_serial;
|
||||||
static std::string s_running_game_title;
|
static std::string s_running_game_title;
|
||||||
|
static std::string s_exe_override;
|
||||||
static const GameDatabase::Entry* s_running_game_entry = nullptr;
|
static const GameDatabase::Entry* s_running_game_entry = nullptr;
|
||||||
static System::GameHash s_running_game_hash;
|
static System::GameHash s_running_game_hash;
|
||||||
|
static System::BootMode s_boot_mode = System::BootMode::FullBoot;
|
||||||
static bool s_running_game_custom_title = false;
|
static bool s_running_game_custom_title = false;
|
||||||
static bool s_was_fast_booted = false;
|
|
||||||
|
|
||||||
static bool s_system_executing = false;
|
static bool s_system_executing = false;
|
||||||
static bool s_system_interrupted = false;
|
static bool s_system_interrupted = false;
|
||||||
|
@ -585,6 +585,11 @@ const std::string& System::GetGameTitle()
|
||||||
return s_running_game_title;
|
return s_running_game_title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::string& System::GetExeOverride()
|
||||||
|
{
|
||||||
|
return s_exe_override;
|
||||||
|
}
|
||||||
|
|
||||||
const GameDatabase::Entry* System::GetGameDatabaseEntry()
|
const GameDatabase::Entry* System::GetGameDatabaseEntry()
|
||||||
{
|
{
|
||||||
return s_running_game_entry;
|
return s_running_game_entry;
|
||||||
|
@ -600,9 +605,9 @@ bool System::IsRunningUnknownGame()
|
||||||
return !s_running_game_entry;
|
return !s_running_game_entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool System::WasFastBooted()
|
System::BootMode System::GetBootMode()
|
||||||
{
|
{
|
||||||
return s_was_fast_booted;
|
return s_boot_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BIOS::ImageInfo* System::GetBIOSImageInfo()
|
const BIOS::ImageInfo* System::GetBIOSImageInfo()
|
||||||
|
@ -1034,7 +1039,7 @@ DiscRegion System::GetRegionForExe(const char* path)
|
||||||
DiscRegion System::GetRegionForPsf(const char* path)
|
DiscRegion System::GetRegionForPsf(const char* path)
|
||||||
{
|
{
|
||||||
PSFLoader::File psf;
|
PSFLoader::File psf;
|
||||||
if (!psf.Load(path))
|
if (!psf.Load(path, nullptr))
|
||||||
return DiscRegion::Other;
|
return DiscRegion::Other;
|
||||||
|
|
||||||
return psf.GetRegion();
|
return psf.GetRegion();
|
||||||
|
@ -1516,18 +1521,27 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||||
// Load CD image up and detect region.
|
// Load CD image up and detect region.
|
||||||
std::unique_ptr<CDImage> disc;
|
std::unique_ptr<CDImage> disc;
|
||||||
DiscRegion disc_region = DiscRegion::NonPS1;
|
DiscRegion disc_region = DiscRegion::NonPS1;
|
||||||
bool do_exe_boot = false;
|
BootMode boot_mode = BootMode::FullBoot;
|
||||||
bool do_psf_boot = false;
|
std::string exe_override;
|
||||||
if (!parameters.filename.empty())
|
if (!parameters.filename.empty())
|
||||||
{
|
{
|
||||||
do_exe_boot = IsExeFileName(parameters.filename);
|
if (IsExeFileName(parameters.filename))
|
||||||
do_psf_boot = (!do_exe_boot && IsPsfFileName(parameters.filename));
|
{
|
||||||
if (do_exe_boot || do_psf_boot)
|
boot_mode = BootMode::BootEXE;
|
||||||
|
exe_override = parameters.filename;
|
||||||
|
}
|
||||||
|
else if (IsPsfFileName(parameters.filename))
|
||||||
|
{
|
||||||
|
boot_mode = BootMode::BootPSF;
|
||||||
|
exe_override = parameters.filename;
|
||||||
|
}
|
||||||
|
if (boot_mode == BootMode::BootEXE || boot_mode == BootMode::BootPSF)
|
||||||
{
|
{
|
||||||
if (s_region == ConsoleRegion::Auto)
|
if (s_region == ConsoleRegion::Auto)
|
||||||
{
|
{
|
||||||
const DiscRegion file_region =
|
const DiscRegion file_region =
|
||||||
(do_exe_boot ? GetRegionForExe(parameters.filename.c_str()) : GetRegionForPsf(parameters.filename.c_str()));
|
((boot_mode == BootMode::BootEXE) ? GetRegionForExe(parameters.filename.c_str()) :
|
||||||
|
GetRegionForPsf(parameters.filename.c_str()));
|
||||||
INFO_LOG("EXE/PSF Region: {}", Settings::GetDiscRegionDisplayName(file_region));
|
INFO_LOG("EXE/PSF Region: {}", Settings::GetDiscRegionDisplayName(file_region));
|
||||||
s_region = GetConsoleRegionForDiscRegion(file_region);
|
s_region = GetConsoleRegionForDiscRegion(file_region);
|
||||||
}
|
}
|
||||||
|
@ -1561,6 +1575,16 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||||
Settings::GetDiscRegionName(disc_region), Settings::GetConsoleRegionName(s_region));
|
Settings::GetDiscRegionName(disc_region), Settings::GetConsoleRegionName(s_region));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool wants_fast_boot =
|
||||||
|
parameters.override_fast_boot.value_or(static_cast<bool>(g_settings.bios_patch_fast_boot));
|
||||||
|
if (wants_fast_boot)
|
||||||
|
{
|
||||||
|
if (disc_region == DiscRegion::NonPS1)
|
||||||
|
ERROR_LOG("Not fast booting non-PS1 disc.");
|
||||||
|
else
|
||||||
|
boot_mode = BootMode::FastBoot;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -1587,7 +1611,6 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||||
UpdateRunningGame(disc ? disc->GetFileName().c_str() : parameters.filename.c_str(), disc.get(), true);
|
UpdateRunningGame(disc ? disc->GetFileName().c_str() : parameters.filename.c_str(), disc.get(), true);
|
||||||
|
|
||||||
// Get boot EXE override.
|
// Get boot EXE override.
|
||||||
std::string exe_boot;
|
|
||||||
if (!parameters.override_exe.empty())
|
if (!parameters.override_exe.empty())
|
||||||
{
|
{
|
||||||
if (!FileSystem::FileExists(parameters.override_exe.c_str()) || !IsExeFileName(parameters.override_exe))
|
if (!FileSystem::FileExists(parameters.override_exe.c_str()) || !IsExeFileName(parameters.override_exe))
|
||||||
|
@ -1601,11 +1624,8 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||||
}
|
}
|
||||||
|
|
||||||
INFO_LOG("Overriding boot executable: '{}'", parameters.override_exe);
|
INFO_LOG("Overriding boot executable: '{}'", parameters.override_exe);
|
||||||
exe_boot = std::move(parameters.override_exe);
|
boot_mode = BootMode::BootEXE;
|
||||||
}
|
exe_override = std::move(parameters.override_exe);
|
||||||
else if (do_exe_boot)
|
|
||||||
{
|
|
||||||
exe_boot = std::move(parameters.filename);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for SBI.
|
// Check for SBI.
|
||||||
|
@ -1621,12 +1641,15 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||||
// Check for resuming with hardcore mode.
|
// Check for resuming with hardcore mode.
|
||||||
if (parameters.disable_achievements_hardcore_mode)
|
if (parameters.disable_achievements_hardcore_mode)
|
||||||
Achievements::DisableHardcoreMode();
|
Achievements::DisableHardcoreMode();
|
||||||
if (!parameters.save_state.empty() && Achievements::IsHardcoreModeActive())
|
if ((!parameters.save_state.empty() || !exe_override.empty()) && Achievements::IsHardcoreModeActive())
|
||||||
{
|
{
|
||||||
|
const bool is_exe_override_boot = parameters.save_state.empty();
|
||||||
bool cancelled;
|
bool cancelled;
|
||||||
if (FullscreenUI::IsInitialized())
|
if (FullscreenUI::IsInitialized())
|
||||||
{
|
{
|
||||||
Achievements::ConfirmHardcoreModeDisableAsync(TRANSLATE("Achievements", "Resuming state"),
|
Achievements::ConfirmHardcoreModeDisableAsync(is_exe_override_boot ?
|
||||||
|
TRANSLATE("Achievements", "Overriding executable") :
|
||||||
|
TRANSLATE("Achievements", "Resuming state"),
|
||||||
[parameters = std::move(parameters)](bool approved) mutable {
|
[parameters = std::move(parameters)](bool approved) mutable {
|
||||||
if (approved)
|
if (approved)
|
||||||
{
|
{
|
||||||
|
@ -1638,7 +1661,9 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
cancelled = !Achievements::ConfirmHardcoreModeDisable(TRANSLATE("Achievements", "Resuming state"));
|
cancelled = !Achievements::ConfirmHardcoreModeDisable(is_exe_override_boot ?
|
||||||
|
TRANSLATE("Achievements", "Overriding executable") :
|
||||||
|
TRANSLATE("Achievements", "Resuming state"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancelled)
|
if (cancelled)
|
||||||
|
@ -1677,45 +1702,14 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||||
if (disc)
|
if (disc)
|
||||||
CDROM::InsertMedia(std::move(disc), disc_region);
|
CDROM::InsertMedia(std::move(disc), disc_region);
|
||||||
|
|
||||||
|
s_boot_mode = boot_mode;
|
||||||
|
s_exe_override = std::move(exe_override);
|
||||||
|
|
||||||
UpdateControllers();
|
UpdateControllers();
|
||||||
UpdateMemoryCardTypes();
|
UpdateMemoryCardTypes();
|
||||||
UpdateMultitaps();
|
UpdateMultitaps();
|
||||||
InternalReset();
|
InternalReset();
|
||||||
|
|
||||||
// Load EXE late after BIOS.
|
|
||||||
if (!exe_boot.empty() && !LoadEXE(exe_boot.c_str()))
|
|
||||||
{
|
|
||||||
Error::SetStringFmt(error, "Failed to load EXE file '{}'", Path::GetFileName(exe_boot));
|
|
||||||
DestroySystem();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else if (do_psf_boot && !PSFLoader::Load(parameters.filename.c_str()))
|
|
||||||
{
|
|
||||||
Error::SetStringFmt(error, "Failed to load PSF file '{}'", Path::GetFileName(parameters.filename));
|
|
||||||
DestroySystem();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply fastboot patch if enabled.
|
|
||||||
if (CDROM::HasMedia() && (parameters.override_fast_boot.has_value() ? parameters.override_fast_boot.value() :
|
|
||||||
g_settings.bios_patch_fast_boot))
|
|
||||||
{
|
|
||||||
if (!CDROM::IsMediaPS1Disc())
|
|
||||||
{
|
|
||||||
ERROR_LOG("Not fast booting non-PS1 disc.");
|
|
||||||
}
|
|
||||||
else if (!s_bios_image_info || !s_bios_image_info->patch_compatible)
|
|
||||||
{
|
|
||||||
ERROR_LOG("Not patching fast boot, as BIOS is not patch compatible.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// TODO: Fast boot without patches...
|
|
||||||
BIOS::PatchBIOSFastBoot(Bus::g_bios, Bus::BIOS_SIZE);
|
|
||||||
s_was_fast_booted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Texture replacement preloading.
|
// Texture replacement preloading.
|
||||||
// TODO: Move this and everything else below OnSystemStarted().
|
// TODO: Move this and everything else below OnSystemStarted().
|
||||||
TextureReplacements::SetGameID(s_running_game_serial);
|
TextureReplacements::SetGameID(s_running_game_serial);
|
||||||
|
@ -1932,7 +1926,8 @@ void System::DestroySystem()
|
||||||
|
|
||||||
s_bios_hash = {};
|
s_bios_hash = {};
|
||||||
s_bios_image_info = nullptr;
|
s_bios_image_info = nullptr;
|
||||||
s_was_fast_booted = false;
|
s_exe_override = {};
|
||||||
|
s_boot_mode = BootMode::FullBoot;
|
||||||
s_cheat_list.reset();
|
s_cheat_list.reset();
|
||||||
|
|
||||||
s_state = State::Shutdown;
|
s_state = State::Shutdown;
|
||||||
|
@ -2474,10 +2469,51 @@ void System::InternalReset()
|
||||||
s_internal_frame_number = 0;
|
s_internal_frame_number = 0;
|
||||||
InterruptExecution();
|
InterruptExecution();
|
||||||
ResetPerformanceCounters();
|
ResetPerformanceCounters();
|
||||||
|
ResetBootMode();
|
||||||
|
|
||||||
Achievements::ResetClient();
|
Achievements::ResetClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void System::ResetBootMode()
|
||||||
|
{
|
||||||
|
// Preserve exe/psf boot.
|
||||||
|
if (s_boot_mode == BootMode::BootEXE || s_boot_mode == BootMode::BootPSF)
|
||||||
|
{
|
||||||
|
DebugAssert(!s_exe_override.empty());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset fast boot flag from settings.
|
||||||
|
const bool wants_fast_boot = (g_settings.bios_patch_fast_boot && CDROM::IsMediaPS1Disc() && s_bios_image_info &&
|
||||||
|
s_bios_image_info->patch_compatible);
|
||||||
|
const System::BootMode new_boot_mode = (s_state != System::State::Starting) ?
|
||||||
|
(wants_fast_boot ? System::BootMode::FastBoot : System::BootMode::FullBoot) :
|
||||||
|
s_boot_mode;
|
||||||
|
if (new_boot_mode != s_boot_mode)
|
||||||
|
{
|
||||||
|
// Need to reload the BIOS to wipe out the patching.
|
||||||
|
Error error;
|
||||||
|
if (!LoadBIOS(&error))
|
||||||
|
ERROR_LOG("Failed to reload BIOS on boot mode change, the system may be unstable: {}", error.GetDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
s_boot_mode = new_boot_mode;
|
||||||
|
if (s_boot_mode == BootMode::FastBoot)
|
||||||
|
{
|
||||||
|
if (s_bios_image_info && s_bios_image_info->patch_compatible)
|
||||||
|
{
|
||||||
|
// Patch BIOS, this sucks.
|
||||||
|
INFO_LOG("Patching BIOS for fast boot.");
|
||||||
|
BIOS::PatchBIOSFastBoot(Bus::g_bios, Bus::BIOS_SIZE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ERROR_LOG("Cannot fast boot, BIOS is incompatible.");
|
||||||
|
s_boot_mode = BootMode::FullBoot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string System::GetMediaPathFromSaveState(const char* path)
|
std::string System::GetMediaPathFromSaveState(const char* path)
|
||||||
{
|
{
|
||||||
std::string ret;
|
std::string ret;
|
||||||
|
@ -3137,138 +3173,6 @@ void System::DoToggleCheats()
|
||||||
Host::OSD_QUICK_DURATION);
|
Host::OSD_QUICK_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool LoadEXEToRAM(const char* filename, bool patch_bios)
|
|
||||||
{
|
|
||||||
std::FILE* fp = FileSystem::OpenCFile(filename, "rb");
|
|
||||||
if (!fp)
|
|
||||||
{
|
|
||||||
ERROR_LOG("Failed to open exe file '{}'", filename);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fseek(fp, 0, SEEK_END);
|
|
||||||
const u32 file_size = static_cast<u32>(std::ftell(fp));
|
|
||||||
std::fseek(fp, 0, SEEK_SET);
|
|
||||||
|
|
||||||
BIOS::PSEXEHeader header;
|
|
||||||
if (std::fread(&header, sizeof(header), 1, fp) != 1 || !BIOS::IsValidPSExeHeader(header, file_size))
|
|
||||||
{
|
|
||||||
ERROR_LOG("'{}' is not a valid PS-EXE", filename);
|
|
||||||
std::fclose(fp);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (header.memfill_size > 0)
|
|
||||||
{
|
|
||||||
const u32 words_to_write = header.memfill_size / 4;
|
|
||||||
u32 address = header.memfill_start & ~UINT32_C(3);
|
|
||||||
for (u32 i = 0; i < words_to_write; i++)
|
|
||||||
{
|
|
||||||
CPU::SafeWriteMemoryWord(address, 0);
|
|
||||||
address += sizeof(u32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const u32 file_data_size = std::min<u32>(file_size - sizeof(BIOS::PSEXEHeader), header.file_size);
|
|
||||||
if (file_data_size >= 4)
|
|
||||||
{
|
|
||||||
std::vector<u32> data_words((file_data_size + 3) / 4);
|
|
||||||
if (std::fread(data_words.data(), file_data_size, 1, fp) != 1)
|
|
||||||
{
|
|
||||||
std::fclose(fp);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const u32 num_words = file_data_size / 4;
|
|
||||||
u32 address = header.load_address;
|
|
||||||
for (u32 i = 0; i < num_words; i++)
|
|
||||||
{
|
|
||||||
CPU::SafeWriteMemoryWord(address, data_words[i]);
|
|
||||||
address += sizeof(u32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fclose(fp);
|
|
||||||
|
|
||||||
// patch the BIOS to jump to the executable directly
|
|
||||||
const u32 r_pc = header.initial_pc;
|
|
||||||
const u32 r_gp = header.initial_gp;
|
|
||||||
const u32 r_sp = header.initial_sp_base + header.initial_sp_offset;
|
|
||||||
const u32 r_fp = header.initial_sp_base + header.initial_sp_offset;
|
|
||||||
return BIOS::PatchBIOSForEXE(Bus::g_bios, Bus::BIOS_SIZE, r_pc, r_gp, r_sp, r_fp);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool System::LoadEXE(const char* filename)
|
|
||||||
{
|
|
||||||
const std::string libps_path(Path::BuildRelativePath(filename, "libps.exe"));
|
|
||||||
if (!libps_path.empty() && FileSystem::FileExists(libps_path.c_str()) && !LoadEXEToRAM(libps_path.c_str(), false))
|
|
||||||
{
|
|
||||||
ERROR_LOG("Failed to load libps.exe from '{}'", libps_path.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return LoadEXEToRAM(filename, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool System::InjectEXEFromBuffer(const void* buffer, u32 buffer_size, bool patch_bios)
|
|
||||||
{
|
|
||||||
const u8* buffer_ptr = static_cast<const u8*>(buffer);
|
|
||||||
const u8* buffer_end = static_cast<const u8*>(buffer) + buffer_size;
|
|
||||||
|
|
||||||
BIOS::PSEXEHeader header;
|
|
||||||
if (buffer_size < sizeof(header))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
std::memcpy(&header, buffer_ptr, sizeof(header));
|
|
||||||
buffer_ptr += sizeof(header);
|
|
||||||
|
|
||||||
const u32 file_size = static_cast<u32>(static_cast<u32>(buffer_end - buffer_ptr));
|
|
||||||
if (!BIOS::IsValidPSExeHeader(header, file_size))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (header.memfill_size > 0)
|
|
||||||
{
|
|
||||||
const u32 words_to_write = header.memfill_size / 4;
|
|
||||||
u32 address = header.memfill_start & ~UINT32_C(3);
|
|
||||||
for (u32 i = 0; i < words_to_write; i++)
|
|
||||||
{
|
|
||||||
CPU::SafeWriteMemoryWord(address, 0);
|
|
||||||
address += sizeof(u32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const u32 file_data_size = std::min<u32>(file_size - sizeof(BIOS::PSEXEHeader), header.file_size);
|
|
||||||
if (file_data_size >= 4)
|
|
||||||
{
|
|
||||||
std::vector<u32> data_words((file_data_size + 3) / 4);
|
|
||||||
if ((buffer_end - buffer_ptr) < file_data_size)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
std::memcpy(data_words.data(), buffer_ptr, file_data_size);
|
|
||||||
|
|
||||||
const u32 num_words = file_data_size / 4;
|
|
||||||
u32 address = header.load_address;
|
|
||||||
for (u32 i = 0; i < num_words; i++)
|
|
||||||
{
|
|
||||||
CPU::SafeWriteMemoryWord(address, data_words[i]);
|
|
||||||
address += sizeof(u32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// patch the BIOS to jump to the executable directly
|
|
||||||
if (patch_bios)
|
|
||||||
{
|
|
||||||
const u32 r_pc = header.initial_pc;
|
|
||||||
const u32 r_gp = header.initial_gp;
|
|
||||||
const u32 r_sp = header.initial_sp_base + header.initial_sp_offset;
|
|
||||||
const u32 r_fp = header.initial_sp_base + header.initial_sp_offset;
|
|
||||||
if (!BIOS::PatchBIOSForEXE(Bus::g_bios, Bus::BIOS_SIZE, r_pc, r_gp, r_sp, r_fp))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if 0
|
#if 0
|
||||||
// currently not used until EXP1 is implemented
|
// currently not used until EXP1 is implemented
|
||||||
|
|
||||||
|
@ -5308,7 +5212,8 @@ void System::RequestDisplaySize(float scale /*= 0.0f*/)
|
||||||
u32 requested_height =
|
u32 requested_height =
|
||||||
std::max<u32>(static_cast<u32>(std::ceil(static_cast<float>(g_gpu->GetCRTCDisplayHeight()) * y_scale * scale)), 1);
|
std::max<u32>(static_cast<u32>(std::ceil(static_cast<float>(g_gpu->GetCRTCDisplayHeight()) * y_scale * scale)), 1);
|
||||||
|
|
||||||
if (g_settings.display_rotation == DisplayRotation::Rotate90 || g_settings.display_rotation == DisplayRotation::Rotate180)
|
if (g_settings.display_rotation == DisplayRotation::Rotate90 ||
|
||||||
|
g_settings.display_rotation == DisplayRotation::Rotate180)
|
||||||
std::swap(requested_width, requested_height);
|
std::swap(requested_width, requested_height);
|
||||||
|
|
||||||
Host::RequestResizeHostDisplay(static_cast<s32>(requested_width), static_cast<s32>(requested_height));
|
Host::RequestResizeHostDisplay(static_cast<s32>(requested_width), static_cast<s32>(requested_height));
|
||||||
|
|
|
@ -109,6 +109,14 @@ enum class State
|
||||||
Stopping,
|
Stopping,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class BootMode
|
||||||
|
{
|
||||||
|
FullBoot,
|
||||||
|
FastBoot,
|
||||||
|
BootEXE,
|
||||||
|
BootPSF,
|
||||||
|
};
|
||||||
|
|
||||||
using GameHash = u64;
|
using GameHash = u64;
|
||||||
|
|
||||||
extern TickCount g_ticks_per_second;
|
extern TickCount g_ticks_per_second;
|
||||||
|
@ -194,10 +202,6 @@ ALWAYS_INLINE_RELEASE TickCount UnscaleTicksToOverclock(TickCount ticks, TickCou
|
||||||
TickCount GetMaxSliceTicks();
|
TickCount GetMaxSliceTicks();
|
||||||
void UpdateOverclock();
|
void UpdateOverclock();
|
||||||
|
|
||||||
/// Injects a PS-EXE into memory at its specified load location. If patch_loader is set, the BIOS will be patched to
|
|
||||||
/// direct execution to this executable.
|
|
||||||
bool InjectEXEFromBuffer(const void* buffer, u32 buffer_size, bool patch_loader = true);
|
|
||||||
|
|
||||||
u32 GetGlobalTickCounter();
|
u32 GetGlobalTickCounter();
|
||||||
u32 GetFrameNumber();
|
u32 GetFrameNumber();
|
||||||
u32 GetInternalFrameNumber();
|
u32 GetInternalFrameNumber();
|
||||||
|
@ -207,10 +211,11 @@ void FrameDone();
|
||||||
const std::string& GetDiscPath();
|
const std::string& GetDiscPath();
|
||||||
const std::string& GetGameSerial();
|
const std::string& GetGameSerial();
|
||||||
const std::string& GetGameTitle();
|
const std::string& GetGameTitle();
|
||||||
|
const std::string& GetExeOverride();
|
||||||
const GameDatabase::Entry* GetGameDatabaseEntry();
|
const GameDatabase::Entry* GetGameDatabaseEntry();
|
||||||
GameHash GetGameHash();
|
GameHash GetGameHash();
|
||||||
bool IsRunningUnknownGame();
|
bool IsRunningUnknownGame();
|
||||||
bool WasFastBooted();
|
BootMode GetBootMode();
|
||||||
|
|
||||||
/// Returns the time elapsed in the current play session.
|
/// Returns the time elapsed in the current play session.
|
||||||
u64 GetSessionPlayedTime();
|
u64 GetSessionPlayedTime();
|
||||||
|
|
Loading…
Reference in a new issue