diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 04f79a618..9f9c02ca6 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -54,6 +54,8 @@ add_library(core memory_card.h pad.cpp pad.h + psf_loader.cpp + psf_loader.h save_state_version.h settings.cpp settings.h @@ -83,7 +85,7 @@ set(RECOMPILER_SRCS target_include_directories(core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") -target_link_libraries(core PUBLIC Threads::Threads common imgui tinyxml2) +target_link_libraries(core PUBLIC Threads::Threads common imgui tinyxml2 zlib) target_link_libraries(core PRIVATE glad stb) if(WIN32) diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index d5c2fb8b7..b4802b6c1 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -77,6 +77,7 @@ + @@ -116,6 +117,7 @@ + @@ -288,7 +290,7 @@ WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false stdcpp17 @@ -313,7 +315,7 @@ WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false stdcpp17 @@ -338,7 +340,7 @@ WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default true false @@ -366,7 +368,7 @@ WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default true false @@ -393,7 +395,7 @@ MaxSpeed true WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false stdcpp17 @@ -419,7 +421,7 @@ MaxSpeed true WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 @@ -446,7 +448,7 @@ MaxSpeed true WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false stdcpp17 @@ -472,7 +474,7 @@ MaxSpeed true WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 71df171e9..f2bbbf7f2 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -40,6 +40,7 @@ + @@ -81,6 +82,7 @@ + diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp index 7a3ddca3d..76ea8e7a8 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -201,6 +201,12 @@ bool GameList::IsExeFileName(const char* path) (StringUtil::Strcasecmp(extension, ".exe") == 0 || StringUtil::Strcasecmp(extension, ".psexe") == 0)); } +bool GameList::IsPsfFileName(const char* path) +{ + const char* extension = std::strrchr(path, '.'); + return (extension && StringUtil::Strcasecmp(extension, ".psf") == 0); +} + static std::string_view GetFileNameFromPath(const char* path) { const char* filename_end = path + std::strlen(path); diff --git a/src/core/game_list.h b/src/core/game_list.h index 19c868cfe..1ea03a5cc 100644 --- a/src/core/game_list.h +++ b/src/core/game_list.h @@ -50,6 +50,9 @@ public: /// Returns true if the filename is a PlayStation executable we can inject. static bool IsExeFileName(const char* path); + /// Returns true if the filename is a Portable Sound Format file we can uncompress/load. + static bool IsPsfFileName(const char* path); + static std::string GetGameCodeForImage(CDImage* cdi); static std::string GetGameCodeForPath(const char* image_path); static DiscRegion GetRegionForCode(std::string_view code); diff --git a/src/core/psf_loader.cpp b/src/core/psf_loader.cpp new file mode 100644 index 000000000..5c9f51382 --- /dev/null +++ b/src/core/psf_loader.cpp @@ -0,0 +1,142 @@ +#include "psf_loader.h" +#include "common/assert.h" +#include "common/file_system.h" +#include "common/log.h" +#include "zlib.h" +#include +#include +Log_SetChannel(PSFLoader); + +namespace PSFLoader { + +std::string File::GetTagString(const char* tag_name, const char* default_value) const +{ + auto it = m_tags.find(tag_name); + if (it == m_tags.end()) + return default_value; + + return it->second; +} + +int File::GetTagInt(const char* tag_name, int default_value) const +{ + auto it = m_tags.find(tag_name); + if (it == m_tags.end()) + return default_value; + + return std::atoi(it->second.c_str()); +} + +float File::GetTagFloat(const char* tag_name, float default_value) const +{ + auto it = m_tags.find(tag_name); + if (it == m_tags.end()) + return default_value; + + return static_cast(std::atof(it->second.c_str())); +} + +bool File::Load(const char* path) +{ + auto fp = FileSystem::OpenManagedCFile(path, "rb"); + if (!fp) + return false; + + // we could mmap this instead + std::fseek(fp.get(), 0, SEEK_END); + const u32 file_size = static_cast(std::ftell(fp.get())); + std::fseek(fp.get(), 0, SEEK_SET); + + std::vector file_data(file_size); + if (std::fread(file_data.data(), 1, file_size, fp.get()) != file_size) + { + Log_ErrorPrintf("Failed to read data from PSF '%s'", path); + return false; + } + + const u8* file_pointer = file_data.data(); + const u8* file_pointer_end = file_data.data() + file_data.size(); + + PSFHeader header; + std::memcpy(&header, file_pointer, sizeof(header)); + file_pointer += sizeof(header); + if (header.id[0] != 'P' || header.id[1] != 'S' || header.id[2] != 'F' || header.version != 0x01 || + header.compressed_program_size == 0 || + (sizeof(header) + header.reserved_area_size + header.compressed_program_size) > file_size) + { + Log_ErrorPrintf("Invalid or incompatible header in PSF '%s'", path); + return false; + } + + file_pointer += header.reserved_area_size; + + m_program_data.resize(MAX_PROGRAM_SIZE); + + z_stream strm = {}; + strm.avail_in = static_cast(file_pointer_end - file_pointer); + strm.next_in = static_cast(const_cast(file_pointer)); + strm.avail_out = static_cast(m_program_data.size()); + strm.next_out = static_cast(m_program_data.data()); + + int err = inflateInit(&strm); + if (err != Z_OK) + { + Log_ErrorPrintf("inflateInit() failed: %d", err); + return false; + } + + // we can do this in one pass because we preallocate the max size + err = inflate(&strm, Z_NO_FLUSH); + if (err != Z_STREAM_END) + { + Log_ErrorPrintf("inflate() failed: %d", err); + inflateEnd(&strm); + return false; + } + else if (strm.total_in != header.compressed_program_size) + { + Log_WarningPrintf("Mismatch between compressed size in header and stream %u/%u", header.compressed_program_size, + static_cast(strm.total_in)); + } + + m_program_data.resize(strm.total_out); + file_pointer += header.compressed_program_size; + inflateEnd(&strm); + + u32 remaining_tag_data = static_cast(file_pointer_end - file_pointer); + static constexpr char tag_signature[] = {'[', 'T', 'A', 'G', ']'}; + if (remaining_tag_data >= sizeof(tag_signature) && + std::memcmp(file_pointer, tag_signature, sizeof(tag_signature)) == 0) + { + file_pointer += sizeof(tag_signature); + + while (file_pointer < file_pointer_end) + { + // skip whitespace + while (file_pointer < file_pointer_end && *file_pointer <= 0x20) + file_pointer++; + + std::string tag_key; + while (file_pointer < file_pointer_end && *file_pointer != '=') + tag_key += (static_cast(*(file_pointer++))); + + // skip = + if (file_pointer < file_pointer_end) + file_pointer++; + + std::string tag_value; + while (file_pointer < file_pointer_end && *file_pointer != '\n') + tag_value += (static_cast(*(file_pointer++))); + + if (!tag_key.empty()) + { + Log_InfoPrintf("PSF Tag: '%s' = '%s'", tag_key.c_str(), tag_value.c_str()); + m_tags.emplace(std::move(tag_key), std::move(tag_value)); + } + } + } + + return true; +} + +} // namespace PSFLoader \ No newline at end of file diff --git a/src/core/psf_loader.h b/src/core/psf_loader.h new file mode 100644 index 000000000..c2b6aa380 --- /dev/null +++ b/src/core/psf_loader.h @@ -0,0 +1,47 @@ +#pragma once +#include "types.h" +#include +#include +#include +#include +#include + +namespace PSFLoader { + +#pragma pack(push, 1) +struct PSFHeader +{ + u8 id[3]; + u8 version; + u32 reserved_area_size; + u32 compressed_program_size; + u32 program_crc32; +}; +#pragma pack(pop) + +class File +{ +public: + using TagMap = std::map; + using ProgramData = std::vector; + + ALWAYS_INLINE const ProgramData& GetProgramData() const { return m_program_data; } + ALWAYS_INLINE const TagMap& GetTagMap() const { return m_tags; } + + std::string GetTagString(const char* tag_name, const char* default_value) const; + int GetTagInt(const char* tag_name, int default_value) const; + float GetTagFloat(const char* tag_name, float default_value) const; + + bool Load(const char* path); + +private: + enum : u32 + { + MAX_PROGRAM_SIZE = 2 * 1024 * 1024 + }; + + ProgramData m_program_data; + TagMap m_tags; +}; + +} // namespace PSFLoader \ No newline at end of file diff --git a/src/core/system.cpp b/src/core/system.cpp index 7824140c2..787d829dc 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -17,6 +17,7 @@ #include "mdec.h" #include "memory_card.h" #include "pad.h" +#include "psf_loader.h" #include "save_state_version.h" #include "sio.h" #include "spu.h" @@ -131,11 +132,14 @@ bool System::Boot(const SystemBootParameters& params) // Load CD image up and detect region. std::unique_ptr media; bool exe_boot = false; + bool psf_boot = false; if (!params.filename.empty()) { exe_boot = GameList::IsExeFileName(params.filename.c_str()); - if (exe_boot) + psf_boot = (!exe_boot && GameList::IsPsfFileName(params.filename.c_str())); + if (exe_boot || psf_boot) { + // TODO: Pull region from PSF if (m_region == ConsoleRegion::Auto) { Log_InfoPrintf("Defaulting to NTSC-U region for executable."); @@ -203,6 +207,11 @@ bool System::Boot(const SystemBootParameters& params) m_host_interface->ReportFormattedError("Failed to load EXE file '%s'", params.filename.c_str()); return false; } + else if (psf_boot && !LoadPSF(params.filename.c_str(), *bios_image)) + { + m_host_interface->ReportFormattedError("Failed to load EXE file '%s'", params.filename.c_str()); + return false; + } // Notify change of disc. UpdateRunningGame(params.filename.c_str(), media.get()); @@ -604,6 +613,69 @@ bool System::LoadEXE(const char* filename, std::vector& bios_image) return BIOS::PatchBIOSForEXE(bios_image, r_pc, r_gp, r_sp, r_fp); } +bool System::LoadEXEFromBuffer(const void* buffer, u32 buffer_size, std::vector& bios_image) +{ + const u8* buffer_ptr = static_cast(buffer); + const u8* buffer_end = static_cast(buffer) + buffer_size; + + BIOS::PSEXEHeader header; + if (buffer_size < sizeof(header)) + return false; + + std::memcpy(&header, buffer_ptr, sizeof(header)); + buffer_ptr += sizeof(header); + + if (!BIOS::IsValidPSExeHeader(header, static_cast(buffer_end - buffer_ptr))) + 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++) + { + m_cpu->SafeWriteMemoryWord(address, 0); + address += sizeof(u32); + } + } + + if (header.file_size >= 4) + { + std::vector data_words((header.file_size + 3) / 4); + if ((buffer_end - buffer_ptr) < header.file_size) + return false; + + std::memcpy(data_words.data(), buffer_ptr, header.file_size); + + const u32 num_words = header.file_size / 4; + u32 address = header.load_address; + for (u32 i = 0; i < num_words; i++) + { + m_cpu->SafeWriteMemoryWord(address, data_words[i]); + address += sizeof(u32); + } + } + + // 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(bios_image, r_pc, r_gp, r_sp, r_fp); +} + +bool System::LoadPSF(const char* filename, std::vector& bios_image) +{ + Log_InfoPrintf("Loading PSF file from '%s'", filename); + + PSFLoader::File psf; + if (!psf.Load(filename)) + return false; + + const std::vector& exe_data = psf.GetProgramData(); + return LoadEXEFromBuffer(exe_data.data(), static_cast(exe_data.size()), bios_image); +} + bool System::SetExpansionROM(const char* filename) { std::FILE* fp = std::fopen(filename, "rb"); diff --git a/src/core/system.h b/src/core/system.h index 4e0ed4408..43703196e 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -120,6 +120,8 @@ public: void ResetPerformanceCounters(); bool LoadEXE(const char* filename, std::vector& bios_image); + bool LoadEXEFromBuffer(const void* buffer, u32 buffer_size, std::vector& bios_image); + bool LoadPSF(const char* filename, std::vector& bios_image); bool SetExpansionROM(const char* filename); // Adds ticks to the global tick counter, simulating the CPU being stalled.