shadPS4/src/emulator.cpp
Stephen Miller cc6af03adf
Lib.SysModule: Proper HLE implementation (#4102)
* Replace sysmodule enums with table

Dumped this from the 12.52 module, using a script I created.

* Better documentation

* Separate from system libraries

That system folder is going to be getting quite large if I left all the sysmodule stuff in there.

* More arrays from library

* Found another preload list

Ghidra really hates decompiling libSceSysmodule, so I didn't notice this one at first.
Also documented specific versions tied to each preload list.

* Start work on implementation

* Some basic implementations

* Initial stub for module loading

Just enough to see if the logic functions appropriately.

* Clang

* sceSysmoduleLoadModule

Now I need to get sceSysmodulePreloadModuleForLibkernel done so that we don't have bugs from not loading internal LLEs.

* sceSysmoduleLoadModuleInternal

* sceSysmodulePreloadModuleForLibkernel

I have successfully broken quite literally everything.
I shall debug this tomorrow.

* Slight fix

* Maybe fix?

* Change log

Enjoy the log spam 😄

* Increased defined stub count

Now that libc and libSceLibcInternal loads later, all the auto stubs are getting consumed by it.

* sceSysmoduleUnloadModule stub

Also a couple fixes. Sysmodule does pass argc and argv to game modules, but only after loading them once to check  binaries.
Shouldn't matter for the most part.

* Clang

* Less stubs

2 thousand is seemingly enough.

* sceSysmoduleLoadModuleInternalWithArg

Doesn't hurt to have, since Apex Legends calls it.

* Oops

* Oops 2

* Rename isModuleLoaded to getModuleHandle

Review comment

* Remove debug game module loads

These cases only trigger when specific sceRegMgr key values are set, and for our purposes, we can treat that case as false.

* Allow preloading to fail

For kalaposfos

* Clang
2026-03-08 21:48:03 +02:00

607 lines
22 KiB
C++

// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <filesystem>
#include <fstream>
#include <iostream>
#include <set>
#include <sstream>
#include <fmt/core.h>
#include <fmt/xchar.h>
#include <hwinfo/hwinfo.h>
#include "common/config.h"
#include "common/debug.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/thread.h"
#include "core/ipc/ipc.h"
#ifdef ENABLE_DISCORD_RPC
#include "common/discord_rpc_handler.h"
#endif
#include "common/elf_info.h"
#include "common/memory_patcher.h"
#include "common/ntapi.h"
#include "common/path_util.h"
#include "common/polyfill_thread.h"
#include "common/scm_rev.h"
#include "common/singleton.h"
#include "core/debugger.h"
#include "core/devtools/widget/module_list.h"
#include "core/emulator_state.h"
#include "core/file_format/psf.h"
#include "core/file_format/trp.h"
#include "core/file_sys/fs.h"
#include "core/libraries/kernel/kernel.h"
#include "core/libraries/libs.h"
#include "core/libraries/np/np_trophy.h"
#include "core/libraries/save_data/save_backup.h"
#include "core/linker.h"
#include "core/memory.h"
#include "emulator.h"
#include "video_core/cache_storage.h"
#include "video_core/renderdoc.h"
#ifdef _WIN32
#include <WinSock2.h>
#endif
#ifndef _WIN32
#include <sys/wait.h>
#include <unistd.h>
#endif
Frontend::WindowSDL* g_window = nullptr;
namespace Core {
Emulator::Emulator() {
// Initialize NT API functions, set high priority and disable WER
#ifdef _WIN32
Common::NtApi::Initialize();
SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS);
SetErrorMode(SetErrorMode(0) | SEM_NOGPFAULTERRORBOX);
// need to init this in order for winsock2 to work
WORD versionWanted = MAKEWORD(2, 2);
WSADATA wsaData;
WSAStartup(versionWanted, &wsaData);
#endif
}
Emulator::~Emulator() {}
s32 ReadCompiledSdkVersion(const std::filesystem::path& file) {
Core::Loader::Elf elf;
elf.Open(file);
if (!elf.IsElfFile()) {
return 0;
}
const auto elf_pheader = elf.GetProgramHeader();
auto i_procparam = std::find_if(elf_pheader.begin(), elf_pheader.end(), [](const auto& entry) {
return entry.p_type == PT_SCE_PROCPARAM;
});
if (i_procparam != elf_pheader.end()) {
Core::OrbisProcParam param{};
elf.LoadSegment(u64(&param), i_procparam->p_offset, i_procparam->p_filesz);
return param.sdk_version;
}
return 0;
}
void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
std::optional<std::filesystem::path> p_game_folder) {
Common::SetCurrentThreadName("shadPS4:Main");
if (waitForDebuggerBeforeRun) {
Debugger::WaitForDebuggerAttach();
}
if (std::filesystem::is_directory(file)) {
file /= "eboot.bin";
}
std::filesystem::path game_folder;
if (p_game_folder.has_value()) {
game_folder = p_game_folder.value();
} else {
game_folder = file.parent_path();
if (const auto game_folder_name = game_folder.filename().string();
game_folder_name.ends_with("-UPDATE") || game_folder_name.ends_with("-patch")) {
// If an executable was launched from a separate update directory,
// use the base game directory as the game folder.
const std::string base_name = game_folder_name.substr(0, game_folder_name.rfind('-'));
const auto base_path = game_folder.parent_path() / base_name;
if (std::filesystem::is_directory(base_path)) {
game_folder = base_path;
}
}
}
std::filesystem::path eboot_name = std::filesystem::relative(file, game_folder);
// Applications expect to be run from /app0 so mount the file's parent path as app0.
auto* mnt = Common::Singleton<Core::FileSys::MntPoints>::Instance();
mnt->Mount(game_folder, "/app0", true);
// Certain games may use /hostapp as well such as CUSA001100
mnt->Mount(game_folder, "/hostapp", true);
const auto param_sfo_path = mnt->GetHostPath("/app0/sce_sys/param.sfo");
const auto param_sfo_exists = std::filesystem::exists(param_sfo_path);
// Load param.sfo details if it exists
std::string id;
std::string title;
std::string app_version;
u32 sdk_version;
u32 fw_version;
Common::PSFAttributes psf_attributes{};
if (param_sfo_exists) {
auto* param_sfo = Common::Singleton<PSF>::Instance();
ASSERT_MSG(param_sfo->Open(param_sfo_path), "Failed to open param.sfo");
const auto content_id = param_sfo->GetString("CONTENT_ID");
const auto title_id = param_sfo->GetString("TITLE_ID");
if (content_id.has_value() && !content_id->empty()) {
id = std::string(*content_id, 7, 9);
} else if (title_id.has_value()) {
id = *title_id;
}
title = param_sfo->GetString("TITLE").value_or("Unknown title");
fw_version = param_sfo->GetInteger("SYSTEM_VER").value_or(0x4700000);
app_version = param_sfo->GetString("APP_VER").value_or("Unknown version");
if (const auto raw_attributes = param_sfo->GetInteger("ATTRIBUTE")) {
psf_attributes.raw = *raw_attributes;
}
// Extract sdk version from pubtool info.
std::string_view pubtool_info =
param_sfo->GetString("PUBTOOLINFO").value_or("Unknown value");
u64 sdk_ver_offset = pubtool_info.find("sdk_ver");
if (sdk_ver_offset == pubtool_info.npos) {
// Default to using firmware version if SDK version is not found.
sdk_version = fw_version;
} else {
// Increment offset to account for sdk_ver= part of string.
sdk_ver_offset += 8;
u64 sdk_ver_len = pubtool_info.find(",", sdk_ver_offset);
if (sdk_ver_len == pubtool_info.npos) {
// If there's no more commas, this is likely the last entry of pubtool info.
// Use string length instead.
sdk_ver_len = pubtool_info.size();
}
sdk_ver_len -= sdk_ver_offset;
std::string sdk_ver_string = pubtool_info.substr(sdk_ver_offset, sdk_ver_len).data();
// Number is stored in base 16.
sdk_version = std::stoi(sdk_ver_string, nullptr, 16);
}
}
auto guest_eboot_path = "/app0/" + eboot_name.generic_string();
const auto eboot_path = mnt->GetHostPath(guest_eboot_path);
auto& game_info = Common::ElfInfo::Instance();
game_info.initialized = true;
game_info.game_serial = id;
game_info.title = title;
game_info.app_ver = app_version;
game_info.firmware_ver = fw_version & 0xFFF00000;
game_info.raw_firmware_ver = fw_version;
game_info.sdk_ver = ReadCompiledSdkVersion(eboot_path);
game_info.psf_attributes = psf_attributes;
const auto pic1_path = mnt->GetHostPath("/app0/sce_sys/pic1.png");
if (std::filesystem::exists(pic1_path)) {
game_info.splash_path = pic1_path;
}
game_info.game_folder = game_folder;
Config::load(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".toml"),
true);
if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) /
(id + ".toml"))) {
EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(true);
} else {
EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(false);
}
// Initialize logging as soon as possible
if (!id.empty() && Config::getSeparateLogFilesEnabled()) {
Common::Log::Initialize(id + ".log");
} else {
Common::Log::Initialize();
}
Common::Log::Start();
if (!std::filesystem::exists(file)) {
LOG_CRITICAL(Loader, "eboot.bin does not exist: {}",
std::filesystem::absolute(file).string());
std::quick_exit(0);
}
LOG_INFO(Loader, "Starting shadps4 emulator v{} ", Common::g_version);
LOG_INFO(Loader, "Revision {}", Common::g_scm_rev);
LOG_INFO(Loader, "Branch {}", Common::g_scm_branch);
LOG_INFO(Loader, "Description {}", Common::g_scm_desc);
LOG_INFO(Loader, "Remote {}", Common::g_scm_remote_url);
const bool has_game_config = std::filesystem::exists(
Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".toml"));
LOG_INFO(Config, "Game-specific config exists: {}", has_game_config);
LOG_INFO(Config, "General LogType: {}", Config::getLogType());
LOG_INFO(Config, "General isIdenticalLogGrouped: {}", Config::groupIdenticalLogs());
LOG_INFO(Config, "General isNeo: {}", Config::isNeoModeConsole());
LOG_INFO(Config, "General isDevKit: {}", Config::isDevKitConsole());
LOG_INFO(Config, "General isConnectedToNetwork: {}", Config::getIsConnectedToNetwork());
LOG_INFO(Config, "General isPsnSignedIn: {}", Config::getPSNSignedIn());
LOG_INFO(Config, "GPU isNullGpu: {}", Config::nullGpu());
LOG_INFO(Config, "GPU readbacksMode: {}", Config::getReadbacksMode());
LOG_INFO(Config, "GPU readbackLinearImages: {}", Config::readbackLinearImages());
LOG_INFO(Config, "GPU directMemoryAccess: {}", Config::directMemoryAccess());
LOG_INFO(Config, "GPU shouldDumpShaders: {}", Config::dumpShaders());
LOG_INFO(Config, "GPU vblankFrequency: {}", Config::vblankFreq());
LOG_INFO(Config, "GPU shouldCopyGPUBuffers: {}", Config::copyGPUCmdBuffers());
LOG_INFO(Config, "Vulkan gpuId: {}", Config::getGpuId());
LOG_INFO(Config, "Vulkan vkValidation: {}", Config::vkValidationEnabled());
LOG_INFO(Config, "Vulkan vkValidationCore: {}", Config::vkValidationCoreEnabled());
LOG_INFO(Config, "Vulkan vkValidationSync: {}", Config::vkValidationSyncEnabled());
LOG_INFO(Config, "Vulkan vkValidationGpu: {}", Config::vkValidationGpuEnabled());
LOG_INFO(Config, "Vulkan crashDiagnostics: {}", Config::getVkCrashDiagnosticEnabled());
LOG_INFO(Config, "Vulkan hostMarkers: {}", Config::getVkHostMarkersEnabled());
LOG_INFO(Config, "Vulkan guestMarkers: {}", Config::getVkGuestMarkersEnabled());
LOG_INFO(Config, "Vulkan rdocEnable: {}", Config::isRdocEnabled());
hwinfo::Memory ram;
hwinfo::OS os;
const auto cpus = hwinfo::getAllCPUs();
for (const auto& cpu : cpus) {
LOG_INFO(Config, "CPU Model: {}", cpu.modelName());
LOG_INFO(Config, "CPU Physical Cores: {}, Logical Cores: {}", cpu.numPhysicalCores(),
cpu.numLogicalCores());
}
LOG_INFO(Config, "Total RAM: {} GB", std::round(ram.total_Bytes() / pow(1024, 3)));
LOG_INFO(Config, "Operating System: {}", os.name());
if (param_sfo_exists) {
LOG_INFO(Loader, "Game id: {} Title: {}", id, title);
LOG_INFO(Loader, "Fw: {:#x} App Version: {}", fw_version, app_version);
LOG_INFO(Loader, "param.sfo SDK version: {:#x}", sdk_version);
LOG_INFO(Loader, "eboot SDK version: {:#x}", game_info.sdk_ver);
LOG_INFO(Loader, "PSVR Supported: {}", (bool)psf_attributes.support_ps_vr.Value());
LOG_INFO(Loader, "PSVR Required: {}", (bool)psf_attributes.require_ps_vr.Value());
}
if (!args.empty()) {
const auto argc = std::min<size_t>(args.size(), 32);
for (auto i = 0; i < argc; i++) {
LOG_INFO(Loader, "Game argument {}: {}", i, args[i]);
}
if (args.size() > 32) {
LOG_ERROR(Loader, "Too many game arguments, only passing the first 32");
}
}
// Create stdin/stdout/stderr
Common::Singleton<FileSys::HandleTable>::Instance()->CreateStdHandles();
// Initialize components
memory = Core::Memory::Instance();
controller = Common::Singleton<Input::GameController>::Instance();
linker = Common::Singleton<Core::Linker>::Instance();
// Load renderdoc module
VideoCore::LoadRenderDoc();
// Initialize patcher and trophies
if (!id.empty()) {
MemoryPatcher::g_game_serial = id;
Libraries::Np::NpTrophy::game_serial = id;
const auto trophyDir =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / id / "TrophyFiles";
if (!std::filesystem::exists(trophyDir)) {
TRP trp;
if (!trp.Extract(game_folder, id)) {
LOG_ERROR(Loader, "Couldn't extract trophies");
}
}
}
std::string game_title = fmt::format("{} - {} <{}>", id, title, app_version);
std::string window_title = "";
std::string remote_url(Common::g_scm_remote_url);
std::string remote_host = Common::GetRemoteNameFromLink();
if (Common::g_is_release) {
if (remote_host == "shadps4-emu" || remote_url.length() == 0) {
window_title = fmt::format("shadPS4 v{} | {}", Common::g_version, game_title);
} else {
window_title =
fmt::format("shadPS4 {}/v{} | {}", remote_host, Common::g_version, game_title);
}
} else {
if (remote_host == "shadps4-emu" || remote_url.length() == 0) {
window_title = fmt::format("shadPS4 v{} {} {} | {}", Common::g_version,
Common::g_scm_branch, Common::g_scm_desc, game_title);
} else {
window_title = fmt::format("shadPS4 v{} {}/{} {} | {}", Common::g_version, remote_host,
Common::g_scm_branch, Common::g_scm_desc, game_title);
}
}
window = std::make_unique<Frontend::WindowSDL>(
Config::getWindowWidth(), Config::getWindowHeight(), controller, window_title);
g_window = window.get();
const auto& mount_data_dir = Common::FS::GetUserPath(Common::FS::PathType::GameDataDir);
mnt->Mount(mount_data_dir, "/data");
// Mounting temp folders
const auto& mount_temp_dir = Common::FS::GetUserPath(Common::FS::PathType::TempDataDir) / id;
if (std::filesystem::exists(mount_temp_dir)) {
// Temp folder should be cleared on each boot.
std::filesystem::remove_all(mount_temp_dir);
}
std::filesystem::create_directory(mount_temp_dir);
mnt->Mount(mount_temp_dir, "/temp0");
mnt->Mount(mount_temp_dir, "/temp");
const auto& mount_download_dir =
Common::FS::GetUserPath(Common::FS::PathType::DownloadDir) / id;
if (!std::filesystem::exists(mount_download_dir)) {
std::filesystem::create_directory(mount_download_dir);
}
mnt->Mount(mount_download_dir, "/download0");
const auto& mount_captures_dir = Common::FS::GetUserPath(Common::FS::PathType::CapturesDir);
if (!std::filesystem::exists(mount_captures_dir)) {
std::filesystem::create_directory(mount_captures_dir);
}
VideoCore::SetOutputDir(mount_captures_dir, id);
// Mount system fonts
const auto& fonts_dir = Config::getFontsPath();
if (!std::filesystem::exists(fonts_dir)) {
std::filesystem::create_directory(fonts_dir);
}
// Fonts are mounted into the sandboxed system directory, construct the appropriate path.
const char* sandbox_root = Libraries::Kernel::sceKernelGetFsSandboxRandomWord();
std::string guest_font_dir = "/";
guest_font_dir.append(sandbox_root).append("/common/font");
const auto& host_font_dir = fonts_dir / "font";
if (!std::filesystem::exists(host_font_dir)) {
std::filesystem::create_directory(host_font_dir);
}
mnt->Mount(host_font_dir, guest_font_dir);
// There is a second font directory, mount that too.
guest_font_dir.append("2");
const auto& host_font2_dir = fonts_dir / "font2";
if (!std::filesystem::exists(host_font2_dir)) {
std::filesystem::create_directory(host_font2_dir);
}
mnt->Mount(host_font2_dir, guest_font_dir);
if (std::filesystem::is_empty(host_font_dir) || std::filesystem::is_empty(host_font2_dir)) {
LOG_WARNING(Loader, "No dumped system fonts, expect missing text or instability");
}
// Initialize kernel and library facilities.
Libraries::InitHLELibs(&linker->GetHLESymbols());
// Load the module with the linker
if (linker->LoadModule(eboot_path) == -1) {
LOG_CRITICAL(Loader, "Failed to load game's eboot.bin: {}",
Common::FS::PathToUTF8String(std::filesystem::absolute(eboot_path)));
std::quick_exit(0);
}
#ifdef ENABLE_DISCORD_RPC
// Discord RPC
if (Config::getEnableDiscordRPC()) {
auto* rpc = Common::Singleton<DiscordRPCHandler::RPC>::Instance();
if (rpc->getRPCEnabled() == false) {
rpc->init();
}
rpc->setStatusPlaying(game_info.title, id);
}
#endif
if (!id.empty()) {
start_time = std::chrono::steady_clock::now();
std::thread([this, id]() {
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(60));
UpdatePlayTime(id);
start_time = std::chrono::steady_clock::now();
}
}).detach();
}
args.insert(args.begin(), eboot_name.generic_string());
linker->Execute(args);
window->InitTimers();
while (window->IsOpen()) {
window->WaitEvent();
}
UpdatePlayTime(id);
Storage::DataBase::Instance().Close();
std::quick_exit(0);
}
void Emulator::Restart(std::filesystem::path eboot_path,
const std::vector<std::string>& guest_args) {
std::vector<std::string> args;
auto mnt = Common::Singleton<Core::FileSys::MntPoints>::Instance();
auto game_path = mnt->GetHostPath("/app0");
args.push_back("--log-append");
args.push_back("--game");
args.push_back(Common::FS::PathToUTF8String(eboot_path));
args.push_back("--override-root");
args.push_back(Common::FS::PathToUTF8String(game_path));
if (FileSys::MntPoints::ignore_game_patches) {
args.push_back("--ignore-game-patch");
}
if (!MemoryPatcher::patch_file.empty()) {
args.push_back("--patch");
args.push_back(MemoryPatcher::patch_file);
}
args.push_back("--wait-for-pid");
args.push_back(std::to_string(Debugger::GetCurrentPid()));
if (waitForDebuggerBeforeRun) {
args.push_back("--wait-for-debugger");
}
if (guest_args.size() > 0) {
args.push_back("--");
for (const auto& arg : guest_args) {
args.push_back(arg);
}
}
LOG_INFO(Common, "Restarting the emulator with args: {}", fmt::join(args, " "));
Libraries::SaveData::Backup::StopThread();
Common::Log::Denitializer();
auto& ipc = IPC::Instance();
if (ipc.IsEnabled()) {
ipc.SendRestart(args);
while (true) {
std::this_thread::sleep_for(std::chrono::minutes(1));
}
}
#if defined(_WIN32)
std::string cmdline;
// Emulator executable
cmdline += "\"";
cmdline += executableName;
cmdline += "\"";
for (const auto& arg : args) {
cmdline += " \"";
cmdline += arg;
cmdline += "\"";
}
cmdline += "\0";
STARTUPINFOA si{};
si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
bool success = CreateProcessA(nullptr, cmdline.data(), nullptr, nullptr, TRUE, 0, nullptr,
nullptr, &si, &pi);
if (!success) {
std::cerr << "Failed to restart game: {}" << GetLastError() << std::endl;
std::quick_exit(1);
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
#elif defined(__APPLE__) || defined(__linux__)
std::vector<char*> argv;
// Emulator executable
argv.push_back(const_cast<char*>(executableName));
for (const auto& arg : args) {
argv.push_back(const_cast<char*>(arg.c_str()));
}
argv.push_back(nullptr);
pid_t pid = fork();
if (pid == 0) {
// Child process - execute the new instance
execvp(executableName, argv.data());
std::cerr << "Failed to restart game: execvp failed" << std::endl;
std::quick_exit(1);
} else if (pid < 0) {
std::cerr << "Failed to restart game: fork failed" << std::endl;
std::quick_exit(1);
}
#else
#error "Unsupported platform"
#endif
std::quick_exit(0);
}
void Emulator::UpdatePlayTime(const std::string& serial) {
const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
const auto filePath = (user_dir / "play_time.txt").string();
std::ifstream in(filePath);
if (!in && !std::ofstream(filePath)) {
LOG_INFO(Loader, "Error opening play_time.txt");
return;
}
auto end_time = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::seconds>(end_time - start_time);
int total_seconds = static_cast<int>(duration.count());
std::vector<std::string> lines;
std::string line;
while (std::getline(in, line)) {
lines.push_back(line);
}
in.close();
int accumulated_seconds = 0;
bool found = false;
for (const auto& l : lines) {
std::istringstream iss(l);
std::string s, time_str;
if (iss >> s >> time_str && s == serial) {
int h, m, s_;
char c1, c2;
std::istringstream ts(time_str);
if (ts >> h >> c1 >> m >> c2 >> s_ && c1 == ':' && c2 == ':') {
accumulated_seconds = h * 3600 + m * 60 + s_;
found = true;
break;
}
}
}
accumulated_seconds += total_seconds;
int hours = accumulated_seconds / 3600;
int minutes = (accumulated_seconds % 3600) / 60;
int seconds = accumulated_seconds % 60;
std::string playTimeSaved = fmt::format("{:d}:{:02d}:{:02d}", hours, minutes, seconds);
std::ofstream outfile(filePath, std::ios::trunc);
bool lineUpdated = false;
for (const auto& l : lines) {
std::istringstream iss(l);
std::string s;
if (iss >> s && s == serial) {
outfile << fmt::format("{} {}\n", serial, playTimeSaved);
lineUpdated = true;
} else {
outfile << l << "\n";
}
}
if (!lineUpdated) {
outfile << fmt::format("{} {}\n", serial, playTimeSaved);
}
LOG_INFO(Loader, "Playing time for {}: {}", serial, playTimeSaved);
}
} // namespace Core