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