Merge branch 'main' into user_and_settings
Some checks failed
Build and Release / reuse (push) Has been cancelled
Build and Release / clang-format (push) Has been cancelled
Build and Release / get-info (push) Has been cancelled
Build and Release / windows-sdl (push) Has been cancelled
Build and Release / macos-sdl (push) Has been cancelled
Build and Release / linux-sdl (push) Has been cancelled
Build and Release / linux-sdl-gcc (push) Has been cancelled
Build and Release / pre-release (push) Has been cancelled

This commit is contained in:
georgemoralis 2026-01-21 15:39:20 +02:00 committed by GitHub
commit 7709e7db7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 997 additions and 151 deletions

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
# SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
# Version 3.24 needed for FetchContent OVERRIDE_FIND_PACKAGE
@ -744,6 +744,8 @@ set(COMMON src/common/logging/backend.cpp
src/common/memory_patcher.cpp
${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp
src/common/scm_rev.h
src/common/key_manager.cpp
src/common/key_manager.h
)
if (ENABLE_DISCORD_RPC)
@ -787,6 +789,8 @@ set(CORE src/core/aerolib/stubs.cpp
src/core/file_format/playgo_chunk.h
src/core/file_format/trp.cpp
src/core/file_format/trp.h
src/core/file_format/npbind.cpp
src/core/file_format/npbind.h
src/core/file_sys/fs.cpp
src/core/file_sys/fs.h
src/core/ipc/ipc.cpp

View File

@ -150,7 +150,7 @@ The following firmware modules are supported and must be placed in shadPS4's `sy
| libSceCesCs.sprx | libSceFont.sprx | libSceFontFt.sprx | libSceFreeTypeOt.sprx |
| libSceJpegDec.sprx | libSceJpegEnc.sprx | libSceJson.sprx | libSceJson2.sprx |
| libSceLibcInternal.sprx | libSceNgs2.sprx | libScePngEnc.sprx | libSceRtc.sprx |
| libSceUlt.sprx | | | |
| libSceUlt.sprx | libSceAudiodec.sprx | | |
</div>
> [!Caution]

161
src/common/key_manager.cpp Normal file
View File

@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include "common/logging/log.h"
#include "key_manager.h"
#include "path_util.h"
std::shared_ptr<KeyManager> KeyManager::s_instance = nullptr;
std::mutex KeyManager::s_mutex;
// ------------------- Constructor & Singleton -------------------
KeyManager::KeyManager() {
SetDefaultKeys();
}
KeyManager::~KeyManager() {
SaveToFile();
}
std::shared_ptr<KeyManager> KeyManager::GetInstance() {
std::lock_guard<std::mutex> lock(s_mutex);
if (!s_instance)
s_instance = std::make_shared<KeyManager>();
return s_instance;
}
void KeyManager::SetInstance(std::shared_ptr<KeyManager> instance) {
std::lock_guard<std::mutex> lock(s_mutex);
s_instance = instance;
}
// ------------------- Load / Save -------------------
bool KeyManager::LoadFromFile() {
try {
const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
const auto keysPath = userDir / "keys.json";
if (!std::filesystem::exists(keysPath)) {
SetDefaultKeys();
SaveToFile();
LOG_DEBUG(KeyManager, "Created default key file: {}", keysPath.string());
return true;
}
std::ifstream file(keysPath);
if (!file.is_open()) {
LOG_ERROR(KeyManager, "Could not open key file: {}", keysPath.string());
return false;
}
json j;
file >> j;
SetDefaultKeys(); // start from defaults
if (j.contains("TrophyKeySet"))
j.at("TrophyKeySet").get_to(m_keys.TrophyKeySet);
LOG_DEBUG(KeyManager, "Successfully loaded keys from: {}", keysPath.string());
return true;
} catch (const std::exception& e) {
LOG_ERROR(KeyManager, "Error loading keys, using defaults: {}", e.what());
SetDefaultKeys();
return false;
}
}
bool KeyManager::SaveToFile() {
try {
const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
const auto keysPath = userDir / "keys.json";
json j;
KeysToJson(j);
std::ofstream file(keysPath);
if (!file.is_open()) {
LOG_ERROR(KeyManager, "Could not open key file for writing: {}", keysPath.string());
return false;
}
file << std::setw(4) << j;
file.flush();
if (file.fail()) {
LOG_ERROR(KeyManager, "Failed to write keys to: {}", keysPath.string());
return false;
}
LOG_DEBUG(KeyManager, "Successfully saved keys to: {}", keysPath.string());
return true;
} catch (const std::exception& e) {
LOG_ERROR(KeyManager, "Error saving keys: {}", e.what());
return false;
}
}
// ------------------- JSON conversion -------------------
void KeyManager::KeysToJson(json& j) const {
j = m_keys;
}
void KeyManager::JsonToKeys(const json& j) {
json current = m_keys; // serialize current defaults
current.update(j); // merge only fields present in file
m_keys = current.get<AllKeys>(); // deserialize back
}
// ------------------- Defaults / Checks -------------------
void KeyManager::SetDefaultKeys() {
m_keys = AllKeys{};
}
bool KeyManager::HasKeys() const {
return !m_keys.TrophyKeySet.ReleaseTrophyKey.empty();
}
// ------------------- Hex conversion -------------------
std::vector<u8> KeyManager::HexStringToBytes(const std::string& hexStr) {
std::vector<u8> bytes;
if (hexStr.empty())
return bytes;
if (hexStr.size() % 2 != 0)
throw std::runtime_error("Invalid hex string length");
bytes.reserve(hexStr.size() / 2);
auto hexCharToInt = [](char c) -> u8 {
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
throw std::runtime_error("Invalid hex character");
};
for (size_t i = 0; i < hexStr.size(); i += 2) {
u8 high = hexCharToInt(hexStr[i]);
u8 low = hexCharToInt(hexStr[i + 1]);
bytes.push_back((high << 4) | low);
}
return bytes;
}
std::string KeyManager::BytesToHexString(const std::vector<u8>& bytes) {
static const char hexDigits[] = "0123456789ABCDEF";
std::string hexStr;
hexStr.reserve(bytes.size() * 2);
for (u8 b : bytes) {
hexStr.push_back(hexDigits[(b >> 4) & 0xF]);
hexStr.push_back(hexDigits[b & 0xF]);
}
return hexStr;
}

79
src/common/key_manager.h Normal file
View File

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include <filesystem>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "common/types.h"
#include "nlohmann/json.hpp"
using json = nlohmann::json;
class KeyManager {
public:
// ------------------- Nested keysets -------------------
struct TrophyKeySet {
std::vector<u8> ReleaseTrophyKey;
};
struct AllKeys {
KeyManager::TrophyKeySet TrophyKeySet;
};
// ------------------- Construction -------------------
KeyManager();
~KeyManager();
// ------------------- Singleton -------------------
static std::shared_ptr<KeyManager> GetInstance();
static void SetInstance(std::shared_ptr<KeyManager> instance);
// ------------------- File operations -------------------
bool LoadFromFile();
bool SaveToFile();
// ------------------- Key operations -------------------
void SetDefaultKeys();
bool HasKeys() const;
// ------------------- Getters / Setters -------------------
const AllKeys& GetAllKeys() const {
return m_keys;
}
void SetAllKeys(const AllKeys& keys) {
m_keys = keys;
}
static std::vector<u8> HexStringToBytes(const std::string& hexStr);
static std::string BytesToHexString(const std::vector<u8>& bytes);
private:
void KeysToJson(json& j) const;
void JsonToKeys(const json& j);
AllKeys m_keys{};
static std::shared_ptr<KeyManager> s_instance;
static std::mutex s_mutex;
};
// ------------------- NLOHMANN macros -------------------
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::TrophyKeySet, ReleaseTrophyKey)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::AllKeys, TrophyKeySet)
namespace nlohmann {
template <>
struct adl_serializer<std::vector<u8>> {
static void to_json(json& j, const std::vector<u8>& vec) {
j = KeyManager::BytesToHexString(vec);
}
static void from_json(const json& j, std::vector<u8>& vec) {
vec = KeyManager::HexStringToBytes(j.get<std::string>());
}
};
} // namespace nlohmann

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -159,6 +160,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
CLS(ImGui) \
CLS(Input) \
CLS(Tty) \
CLS(KeyManager) \
CLS(Loader)
// GetClassName is a macro defined by Windows.h, grrr...

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2023 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -127,6 +128,7 @@ enum class Class : u8 {
Loader, ///< ROM loader
Input, ///< Input emulation
Tty, ///< Debug output from emu
KeyManager, ///< Key management system
Count ///< Total number of logging classes
};

View File

@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <vector>
#include "npbind.h"
bool NPBindFile::Load(const std::string& path) {
Clear(); // Clear any existing data
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f)
return false;
std::streamsize sz = f.tellg();
if (sz <= 0)
return false;
f.seekg(0, std::ios::beg);
std::vector<u8> buf(static_cast<size_t>(sz));
if (!f.read(reinterpret_cast<char*>(buf.data()), sz))
return false;
const u64 size = buf.size();
if (size < sizeof(NpBindHeader))
return false;
// Read header
memcpy(&m_header, buf.data(), sizeof(NpBindHeader));
if (m_header.magic != NPBIND_MAGIC)
return false;
// offset start of bodies
size_t offset = sizeof(NpBindHeader);
m_bodies.reserve(static_cast<size_t>(m_header.num_entries));
// For each body: read 4 TLV entries then skip padding (0x98 = 152 bytes)
const u64 body_padding = 0x98; // 152
for (u64 bi = 0; bi < m_header.num_entries; ++bi) {
// Ensure we have room for 4 entries' headers at least
if (offset + 4 * 4 > size)
return false; // 4 entries x (type+size)
NPBindBody body;
// helper lambda to read one entry
auto read_entry = [&](NPBindEntryRaw& e) -> bool {
if (offset + 4 > size)
return false;
memcpy(&e.type, &buf[offset], 2);
memcpy(&e.size, &buf[offset + 2], 2);
offset += 4;
if (offset + e.size > size)
return false;
e.data.assign(buf.begin() + offset, buf.begin() + offset + e.size);
offset += e.size;
return true;
};
// read 4 entries in order
if (!read_entry(body.npcommid))
return false;
if (!read_entry(body.trophy))
return false;
if (!read_entry(body.unk1))
return false;
if (!read_entry(body.unk2))
return false;
// skip fixed padding after body if present (but don't overrun)
if (offset + body_padding <= size) {
offset += body_padding;
} else {
// If padding not fully present, allow file to end (some variants may omit)
offset = size;
}
m_bodies.push_back(std::move(body));
}
// Read digest if available
if (size >= 20) {
// Digest is typically the last 20 bytes, independent of offset
memcpy(m_digest, &buf[size - 20], 20);
} else {
memset(m_digest, 0, 20);
}
return true;
}
std::vector<std::string> NPBindFile::GetNpCommIds() const {
std::vector<std::string> npcommids;
npcommids.reserve(m_bodies.size());
for (const auto& body : m_bodies) {
// Convert binary data to string directly
if (!body.npcommid.data.empty()) {
std::string raw_string(reinterpret_cast<const char*>(body.npcommid.data.data()),
body.npcommid.data.size());
npcommids.push_back(raw_string);
}
}
return npcommids;
}

View File

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "common/endian.h"
#include "common/types.h"
#define NPBIND_MAGIC 0xD294A018u
#pragma pack(push, 1)
struct NpBindHeader {
u32_be magic;
u32_be version;
u64_be file_size;
u64_be entry_size;
u64_be num_entries;
char padding[0x60]; // 96 bytes
};
#pragma pack(pop)
struct NPBindEntryRaw {
u16_be type;
u16_be size; // includes internal padding
std::vector<u8> data;
};
struct NPBindBody {
NPBindEntryRaw npcommid; // expected type 0x0010, size 12
NPBindEntryRaw trophy; // expected type 0x0011, size 12
NPBindEntryRaw unk1; // expected type 0x0012, size 176
NPBindEntryRaw unk2; // expected type 0x0013, size 16
// The 0x98 padding after these entries is skipped while parsing
};
class NPBindFile {
private:
NpBindHeader m_header;
std::vector<NPBindBody> m_bodies;
u8 m_digest[20]; // zeroed if absent
public:
NPBindFile() {
memset(m_digest, 0, sizeof(m_digest));
}
// Load from file
bool Load(const std::string& path);
// Accessors
const NpBindHeader& Header() const {
return m_header;
}
const std::vector<NPBindBody>& Bodies() const {
return m_bodies;
}
const u8* Digest() const {
return m_digest;
}
// Get npcommid data
std::vector<std::string> GetNpCommIds() const;
// Get specific body
const NPBindBody& GetBody(size_t index) const {
return m_bodies.at(index);
}
// Get number of bodies
u64 BodyCount() const {
return m_bodies.size();
}
// Check if file was loaded successfully
bool IsValid() const {
return m_header.magic == NPBIND_MAGIC;
}
// Clear all data
void Clear() {
m_header = NpBindHeader{};
m_bodies.clear();
memset(m_digest, 0, sizeof(m_digest));
}
};

View File

@ -1,44 +1,31 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/aes.h"
#include "common/config.h"
#include "common/key_manager.h"
#include "common/logging/log.h"
#include "common/path_util.h"
#include "core/file_format/npbind.h"
#include "core/file_format/trp.h"
static void DecryptEFSM(std::span<u8, 16> trophyKey, std::span<u8, 16> NPcommID,
std::span<u8, 16> efsmIv, std::span<u8> ciphertext,
static void DecryptEFSM(std::span<const u8, 16> trophyKey, std::span<const u8, 16> NPcommID,
std::span<const u8, 16> efsmIv, std::span<const u8> ciphertext,
std::span<u8> decrypted) {
// Step 1: Encrypt NPcommID
std::array<u8, 16> trophyIv{};
std::array<u8, 16> trpKey;
// Convert spans to pointers for the aes functions
aes::encrypt_cbc(NPcommID.data(), NPcommID.size(), trophyKey.data(), trophyKey.size(),
trophyIv.data(), trpKey.data(), trpKey.size(), false);
// Step 2: Decrypt EFSM
aes::decrypt_cbc(ciphertext.data(), ciphertext.size(), trpKey.data(), trpKey.size(),
efsmIv.data(), decrypted.data(), decrypted.size(), nullptr);
const_cast<u8*>(efsmIv.data()), decrypted.data(), decrypted.size(), nullptr);
}
TRP::TRP() = default;
TRP::~TRP() = default;
void TRP::GetNPcommID(const std::filesystem::path& trophyPath, int index) {
std::filesystem::path trpPath = trophyPath / "sce_sys/npbind.dat";
Common::FS::IOFile npbindFile(trpPath, Common::FS::FileAccessMode::Read);
if (!npbindFile.IsOpen()) {
LOG_CRITICAL(Common_Filesystem, "Failed to open npbind.dat file");
return;
}
if (!npbindFile.Seek(0x84 + (index * 0x180))) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to NPbind offset");
return;
}
npbindFile.ReadRaw<u8>(np_comm_id.data(), 12);
std::fill(np_comm_id.begin() + 12, np_comm_id.end(), 0); // fill with 0, we need 16 bytes.
}
static void removePadding(std::vector<u8>& vec) {
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
if (*it == '>') {
@ -59,95 +46,236 @@ static void hexToBytes(const char* hex, unsigned char* dst) {
bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string titleId) {
std::filesystem::path gameSysDir = trophyPath / "sce_sys/trophy/";
if (!std::filesystem::exists(gameSysDir)) {
LOG_CRITICAL(Common_Filesystem, "Game sce_sys directory doesn't exist");
LOG_WARNING(Common_Filesystem, "Game trophy directory doesn't exist");
return false;
}
const auto user_key_str = Config::getTrophyKey();
if (user_key_str.size() != 32) {
const auto& user_key_vec =
KeyManager::GetInstance()->GetAllKeys().TrophyKeySet.ReleaseTrophyKey;
if (user_key_vec.size() != 16) {
LOG_INFO(Common_Filesystem, "Trophy decryption key is not specified");
return false;
}
std::array<u8, 16> user_key{};
hexToBytes(user_key_str.c_str(), user_key.data());
std::copy(user_key_vec.begin(), user_key_vec.end(), user_key.begin());
for (int index = 0; const auto& it : std::filesystem::directory_iterator(gameSysDir)) {
if (it.is_regular_file()) {
GetNPcommID(trophyPath, index);
// Load npbind.dat using the new class
std::filesystem::path npbindPath = trophyPath / "sce_sys/npbind.dat";
NPBindFile npbind;
if (!npbind.Load(npbindPath.string())) {
LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file");
}
auto npCommIds = npbind.GetNpCommIds();
if (npCommIds.empty()) {
LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat");
}
bool success = true;
int trpFileIndex = 0;
try {
// Process each TRP file in the trophy directory
for (const auto& it : std::filesystem::directory_iterator(gameSysDir)) {
if (!it.is_regular_file() || it.path().extension() != ".trp") {
continue; // Skip non-TRP files
}
// Get NPCommID for this TRP file (if available)
std::string npCommId;
if (trpFileIndex < static_cast<int>(npCommIds.size())) {
npCommId = npCommIds[trpFileIndex];
LOG_DEBUG(Common_Filesystem, "Using NPCommID: {} for {}", npCommId,
it.path().filename().string());
} else {
LOG_WARNING(Common_Filesystem, "No NPCommID found for TRP file index {}",
trpFileIndex);
}
Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read);
if (!file.IsOpen()) {
LOG_CRITICAL(Common_Filesystem, "Unable to open trophy file for read");
return false;
LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.path().string());
success = false;
continue;
}
TrpHeader header;
file.Read(header);
if (header.magic != 0xDCA24D00) {
LOG_CRITICAL(Common_Filesystem, "Wrong trophy magic number");
return false;
if (!file.Read(header)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}",
it.path().string());
success = false;
continue;
}
if (header.magic != TRP_MAGIC) {
LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.path().string());
success = false;
continue;
}
s64 seekPos = sizeof(TrpHeader);
std::filesystem::path trpFilesPath(
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId /
"TrophyFiles" / it.path().stem());
std::filesystem::create_directories(trpFilesPath / "Icons");
std::filesystem::create_directory(trpFilesPath / "Xml");
// Create output directories
if (!std::filesystem::create_directories(trpFilesPath / "Icons") ||
!std::filesystem::create_directories(trpFilesPath / "Xml")) {
LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", titleId);
success = false;
continue;
}
// Process each entry in the TRP file
for (int i = 0; i < header.entry_num; i++) {
if (!file.Seek(seekPos)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset");
return false;
LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset");
success = false;
break;
}
seekPos += (s64)header.entry_size;
seekPos += static_cast<s64>(header.entry_size);
TrpEntry entry;
file.Read(entry);
std::string_view name(entry.entry_name);
if (entry.flag == 0) { // PNG
if (!file.Seek(entry.entry_pos)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset");
return false;
}
std::vector<u8> icon(entry.entry_len);
file.Read(icon);
Common::FS::IOFile::WriteBytes(trpFilesPath / "Icons" / name, icon);
if (!file.Read(entry)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP entry");
success = false;
break;
}
if (entry.flag == 3 && np_comm_id[0] == 'N' &&
np_comm_id[1] == 'P') { // ESFM, encrypted.
if (!file.Seek(entry.entry_pos)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset");
return false;
std::string_view name(entry.entry_name);
if (entry.flag == ENTRY_FLAG_PNG) {
if (!ProcessPngEntry(file, entry, trpFilesPath, name)) {
success = false;
// Continue with next entry
}
file.Read(esfmIv); // get iv key.
// Skip the first 16 bytes which are the iv key on every entry as we want a
// clean xml file.
std::vector<u8> ESFM(entry.entry_len - iv_len);
std::vector<u8> XML(entry.entry_len - iv_len);
if (!file.Seek(entry.entry_pos + iv_len)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry + iv offset");
return false;
}
file.Read(ESFM);
DecryptEFSM(user_key, np_comm_id, esfmIv, ESFM, XML); // decrypt
removePadding(XML);
std::string xml_name = entry.entry_name;
size_t pos = xml_name.find("ESFM");
if (pos != std::string::npos)
xml_name.replace(pos, xml_name.length(), "XML");
std::filesystem::path path = trpFilesPath / "Xml" / xml_name;
size_t written = Common::FS::IOFile::WriteBytes(path, XML);
if (written != XML.size()) {
LOG_CRITICAL(
Common_Filesystem,
"Trophy XML {} write failed, wanted to write {} bytes, wrote {}",
fmt::UTF(path.u8string()), XML.size(), written);
} else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) {
// Check if we have a valid NPCommID for decryption
if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') {
if (!ProcessEncryptedXmlEntry(file, entry, trpFilesPath, name, user_key,
npCommId)) {
success = false;
// Continue with next entry
}
} else {
LOG_WARNING(Common_Filesystem,
"Skipping encrypted XML entry - invalid NPCommID");
// Skip this entry but continue
}
} else {
LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}",
static_cast<unsigned int>(entry.flag), name);
}
}
trpFileIndex++;
}
index++;
} catch (const std::filesystem::filesystem_error& e) {
LOG_CRITICAL(Common_Filesystem, "Filesystem error during trophy extraction: {}", e.what());
return false;
} catch (const std::exception& e) {
LOG_CRITICAL(Common_Filesystem, "Error during trophy extraction: {}", e.what());
return false;
}
if (success) {
LOG_INFO(Common_Filesystem, "Successfully extracted {} trophy files for {}", trpFileIndex,
titleId);
}
return success;
}
bool TRP::ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name) {
if (!file.Seek(entry.entry_pos)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to PNG entry offset");
return false;
}
std::vector<u8> icon(entry.entry_len);
if (!file.Read(icon)) {
LOG_ERROR(Common_Filesystem, "Failed to read PNG data");
return false;
}
auto outputFile = outputPath / "Icons" / name;
size_t written = Common::FS::IOFile::WriteBytes(outputFile, icon);
if (written != icon.size()) {
LOG_ERROR(Common_Filesystem, "PNG write failed: wanted {} bytes, wrote {}", icon.size(),
written);
return false;
}
return true;
}
bool TRP::ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name,
const std::array<u8, 16>& user_key,
const std::string& npCommId) {
constexpr size_t IV_LEN = 16;
if (!file.Seek(entry.entry_pos)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted XML entry offset");
return false;
}
std::array<u8, IV_LEN> esfmIv;
if (!file.Read(esfmIv)) {
LOG_ERROR(Common_Filesystem, "Failed to read IV for encrypted XML");
return false;
}
if (entry.entry_len <= IV_LEN) {
LOG_ERROR(Common_Filesystem, "Encrypted XML entry too small");
return false;
}
// Skip to the encrypted data (after IV)
if (!file.Seek(entry.entry_pos + IV_LEN)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted data");
return false;
}
std::vector<u8> ESFM(entry.entry_len - IV_LEN);
std::vector<u8> XML(entry.entry_len - IV_LEN);
if (!file.Read(ESFM)) {
LOG_ERROR(Common_Filesystem, "Failed to read encrypted XML data");
return false;
}
// Decrypt the data - FIX: Don't check return value since DecryptEFSM returns void
std::span<const u8, 16> key_span(user_key);
// Convert npCommId string to span (pad or truncate to 16 bytes)
std::array<u8, 16> npcommid_array{};
size_t copy_len = std::min(npCommId.size(), npcommid_array.size());
std::memcpy(npcommid_array.data(), npCommId.data(), copy_len);
std::span<const u8, 16> npcommid_span(npcommid_array);
DecryptEFSM(key_span, npcommid_span, esfmIv, ESFM, XML);
// Remove padding
removePadding(XML);
// Create output filename (replace ESFM with XML)
std::string xml_name(entry.entry_name);
size_t pos = xml_name.find("ESFM");
if (pos != std::string::npos) {
xml_name.replace(pos, 4, "XML");
}
auto outputFile = outputPath / "Xml" / xml_name;
size_t written = Common::FS::IOFile::WriteBytes(outputFile, XML);
if (written != XML.size()) {
LOG_ERROR(Common_Filesystem, "XML write failed: wanted {} bytes, wrote {}", XML.size(),
written);
return false;
}
return true;
}

View File

@ -8,6 +8,10 @@
#include "common/io_file.h"
#include "common/types.h"
static constexpr u32 TRP_MAGIC = 0xDCA24D00;
static constexpr u8 ENTRY_FLAG_PNG = 0;
static constexpr u8 ENTRY_FLAG_ENCRYPTED_XML = 3;
struct TrpHeader {
u32_be magic; // (0xDCA24D00)
u32_be version;
@ -33,9 +37,14 @@ public:
TRP();
~TRP();
bool Extract(const std::filesystem::path& trophyPath, const std::string titleId);
void GetNPcommID(const std::filesystem::path& trophyPath, int index);
private:
bool ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name);
bool ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name,
const std::array<u8, 16>& user_key, const std::string& npCommId);
std::vector<u8> NPcommID = std::vector<u8>(12);
std::array<u8, 16> np_comm_id{};
std::array<u8, 16> esfmIv{};

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
@ -34,7 +34,7 @@ u32 GetChannelMask(u32 num_channels) {
case 8:
return ORBIS_AJM_CHANNELMASK_7POINT1;
default:
UNREACHABLE();
UNREACHABLE_MSG("Unexpected number of channels: {}", num_channels);
}
}

View File

@ -5,20 +5,45 @@
#include "ajm_aac.h"
#include "ajm_result.h"
#include <aacdecoder_lib.h>
// using this internal header to manually configure the decoder in RAW mode
#include "externals/aacdec/fdk-aac/libAACdec/src/aacdecoder.h"
#include <aacdecoder_lib.h>
#include <magic_enum/magic_enum.hpp>
#include <algorithm> // std::transform
#include <iterator> // std::back_inserter
#include <limits>
namespace Libraries::Ajm {
std::span<const s16> AjmAacDecoder::GetOuputPcm(u32 skipped_pcm, u32 max_pcm) const {
const auto pcm_data = std::span(m_pcm_buffer).subspan(skipped_pcm);
return pcm_data.subspan(0, std::min<u32>(pcm_data.size(), max_pcm));
}
template <>
size_t AjmAacDecoder::WriteOutputSamples<float>(SparseOutputBuffer& out, std::span<const s16> pcm) {
if (pcm.empty()) {
return 0;
}
m_resample_buffer.clear();
constexpr float inv_scale = 1.0f / std::numeric_limits<s16>::max();
std::transform(pcm.begin(), pcm.end(), std::back_inserter(m_resample_buffer),
[](auto sample) { return float(sample) * inv_scale; });
return out.Write(std::span(m_resample_buffer));
}
AjmAacDecoder::AjmAacDecoder(AjmFormatEncoding format, AjmAacCodecFlags flags, u32 channels)
: m_format(format), m_flags(flags), m_channels(channels), m_pcm_buffer(2048 * 8),
m_skip_frames(True(flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2) {}
: m_format(format), m_flags(flags), m_channels(channels), m_pcm_buffer(1024 * 8),
m_skip_frames(True(flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2) {
m_resample_buffer.reserve(m_pcm_buffer.size());
}
AjmAacDecoder::~AjmAacDecoder() {
aacDecoder_Close(m_decoder);
if (m_decoder) {
aacDecoder_Close(m_decoder);
}
}
TRANSPORT_TYPE TransportTypeFromConfigType(ConfigType config_type) {
@ -98,7 +123,7 @@ AjmSidebandFormat AjmAacDecoder::GetFormat() const {
.num_channels = static_cast<u32>(info->numChannels),
.channel_mask = GetChannelMask(info->numChannels),
.sampl_freq = static_cast<u32>(info->sampleRate),
.sample_encoding = m_format, // AjmFormatEncoding
.sample_encoding = m_format,
.bitrate = static_cast<u32>(info->bitRate),
};
}
@ -130,8 +155,7 @@ DecoderResult AjmAacDecoder::ProcessData(std::span<u8>& input, SparseOutputBuffe
const UINT sizes[] = {static_cast<UINT>(input.size())};
UINT valid = sizes[0];
aacDecoder_Fill(m_decoder, buffers, sizes, &valid);
auto ret = aacDecoder_DecodeFrame(m_decoder, reinterpret_cast<s16*>(m_pcm_buffer.data()),
m_pcm_buffer.size() / 2, 0);
auto ret = aacDecoder_DecodeFrame(m_decoder, m_pcm_buffer.data(), m_pcm_buffer.size(), 0);
switch (ret) {
case AAC_DEC_OK:
@ -167,16 +191,16 @@ DecoderResult AjmAacDecoder::ProcessData(std::span<u8>& input, SparseOutputBuffe
gapless.init.total_samples != 0 ? gapless.current.total_samples : info->aacSamplesPerFrame;
size_t pcm_written = 0;
auto pcm = GetOuputPcm(skip_samples * info->numChannels, max_samples * info->numChannels);
switch (m_format) {
case AjmFormatEncoding::S16:
pcm_written = WriteOutputSamples<s16>(output, skip_samples * info->numChannels,
max_samples * info->numChannels);
pcm_written = output.Write(pcm);
break;
case AjmFormatEncoding::S32:
UNREACHABLE_MSG("NOT IMPLEMENTED");
break;
case AjmFormatEncoding::Float:
UNREACHABLE_MSG("NOT IMPLEMENTED");
pcm_written = WriteOutputSamples<float>(output, pcm);
break;
default:
UNREACHABLE();
@ -191,4 +215,4 @@ DecoderResult AjmAacDecoder::ProcessData(std::span<u8>& input, SparseOutputBuffe
return result;
}
} // namespace Libraries::Ajm
} // namespace Libraries::Ajm

View File

@ -52,22 +52,18 @@ private:
};
template <class T>
size_t WriteOutputSamples(SparseOutputBuffer& output, u32 skipped_pcm, u32 max_pcm) {
std::span<T> pcm_data{reinterpret_cast<T*>(m_pcm_buffer.data()),
m_pcm_buffer.size() / sizeof(T)};
pcm_data = pcm_data.subspan(skipped_pcm);
const auto pcm_size = std::min(u32(pcm_data.size()), max_pcm);
return output.Write(pcm_data.subspan(0, pcm_size));
}
size_t WriteOutputSamples(SparseOutputBuffer& output, std::span<const s16> pcm);
std::span<const s16> GetOuputPcm(u32 skipped_pcm, u32 max_pcm) const;
const AjmFormatEncoding m_format;
const AjmAacCodecFlags m_flags;
const u32 m_channels;
std::vector<u8> m_pcm_buffer;
std::vector<s16> m_pcm_buffer;
std::vector<float> m_resample_buffer;
u32 m_skip_frames = 0;
InitializeParameters m_init_params = {};
AAC_DECODER_INSTANCE* m_decoder = nullptr;
};
} // namespace Libraries::Ajm
} // namespace Libraries::Ajm

View File

@ -280,9 +280,7 @@ AjmJob AjmJobFromBatchBuffer(u32 instance_id, AjmBatchBuffer batch_buffer) {
job.input.resample_parameters = input_batch.Consume<AjmSidebandResampleParameters>();
}
if (True(control_flags & AjmJobControlFlags::Initialize)) {
job.input.init_params = AjmDecAt9InitializeParameters{};
std::memcpy(&job.input.init_params.value(), input_batch.GetCurrent(),
input_batch.BytesRemaining());
job.input.init_params = input_batch.Consume<AjmSidebandInitParameters>();
}
}

View File

@ -21,7 +21,7 @@ namespace Libraries::Ajm {
struct AjmJob {
struct Input {
std::optional<AjmDecAt9InitializeParameters> init_params;
std::optional<AjmSidebandInitParameters> init_params;
std::optional<AjmSidebandResampleParameters> resample_parameters;
std::optional<AjmSidebandStatisticsEngineParameters> statistics_engine_parameters;
std::optional<AjmSidebandFormat> format;

View File

@ -189,7 +189,7 @@ s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray() {
s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* params) {
LOG_DEBUG(Lib_Audio3d, "called");
if (params) {
*params = OrbisAudio3dOpenParameters{
auto default_params = OrbisAudio3dOpenParameters{
.size_this = 0x20,
.granularity = 0x100,
.rate = OrbisAudio3dRate::ORBIS_AUDIO3D_RATE_48000,
@ -197,6 +197,7 @@ s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters*
.queue_depth = 2,
.buffer_mode = OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH,
};
memcpy(params, &default_params, 0x20);
}
return ORBIS_OK;
}
@ -445,7 +446,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortOpen(const OrbisUserServiceUserId user_id,
}
*port_id = id;
std::memcpy(&state->ports[id].parameters, parameters, sizeof(OrbisAudio3dOpenParameters));
std::memcpy(&state->ports[id].parameters, parameters, parameters->size_this);
return ORBIS_OK;
}

View File

@ -30,7 +30,15 @@ AvPlayerSourceType GetSourceType(std::string_view path) {
}
// schema://server.domain/path/to/file.ext/and/beyond -> .ext/and/beyond
auto ext = name.substr(name.rfind('.'));
// Find extension dot
auto dot_pos = name.rfind('.');
if (dot_pos == std::string_view::npos) {
return AvPlayerSourceType::Unknown;
}
// Extract extension (".ext/anything" or ".ext")
auto ext = name.substr(dot_pos);
if (ext.empty()) {
return AvPlayerSourceType::Unknown;
}

View File

@ -115,9 +115,199 @@ Error PS4_SYSV_ABI sceImeDialogGetPanelSize(const OrbisImeDialogParam* param, u3
return Error::OK;
}
int PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended() {
LOG_ERROR(Lib_ImeDialog, "(STUBBED) called");
return ORBIS_OK;
Error PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(const OrbisImeDialogParam* param,
const OrbisImeParamExtended* extended,
u32* width, u32* height) {
if (!param || !width || !height) {
return Error::INVALID_ADDRESS;
}
// Check parameter bounds
if (static_cast<uint32_t>(param->type) > 4) {
return Error::INVALID_ARG;
}
if (extended) {
// Check panel priority for full panel mode (Accent = 3)
if (extended->priority == OrbisImePanelPriority::Accent) {
// Full panel mode - return maximum size
if ((param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) !=
OrbisImeOption::DEFAULT) {
*width = 2560; // For 4K/5K displays
*height = 1440;
} else {
*width = 1920;
*height = 1080;
}
LOG_DEBUG(Lib_ImeDialog, "Full panel mode: width={}, height={}", *width, *height);
return Error::OK;
}
}
// First get the base panel size from the basic function
Error result = sceImeDialogGetPanelSize(param, width, height);
if (result != Error::OK) {
return result;
}
// Adjust based on IME type
switch (param->type) {
case OrbisImeType::Default:
case OrbisImeType::BasicLatin:
case OrbisImeType::Url:
case OrbisImeType::Mail:
// Standard IME types
if ((param->option & OrbisImeOption::PASSWORD) != OrbisImeOption::DEFAULT) {
*height = *height + 20;
}
if ((param->option & OrbisImeOption::MULTILINE) != OrbisImeOption::DEFAULT) {
*height = *height * 3 / 2;
}
break;
case OrbisImeType::Number:
*width = *width * 3 / 4;
*height = *height * 2 / 3;
break;
default:
// Unknown type, use default size
break;
}
// Apply extended options if provided
if (extended) {
// Handle extended option flags
if ((extended->option & OrbisImeExtOption::PRIORITY_FULL_WIDTH) !=
OrbisImeExtOption::DEFAULT) {
// Full width priority
bool use_2k = (param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) !=
OrbisImeOption::DEFAULT;
*width = use_2k ? 1200 : 800;
LOG_DEBUG(Lib_ImeDialog, "Full width priority: width={}", *width);
}
if ((extended->option & OrbisImeExtOption::PRIORITY_FIXED_PANEL) !=
OrbisImeExtOption::DEFAULT) {
// Fixed panel size
*width = 600;
*height = 400;
LOG_DEBUG(Lib_ImeDialog, "Fixed panel: width={}, height={}", *width, *height);
}
switch (extended->priority) {
case OrbisImePanelPriority::Alphabet:
*width = 600;
*height = 400;
break;
case OrbisImePanelPriority::Symbol:
*width = 500;
*height = 300;
break;
case OrbisImePanelPriority::Accent:
// Already handled
break;
case OrbisImePanelPriority::Default:
default:
// Use calculated sizes
break;
}
if ((extended->option & OrbisImeExtOption::INIT_EXT_KEYBOARD_MODE) !=
OrbisImeExtOption::DEFAULT) {
if (extended->ext_keyboard_mode != 0) {
// Check for high-res mode flags
if ((extended->ext_keyboard_mode &
static_cast<uint32_t>(
OrbisImeInitExtKeyboardMode::INPUT_METHOD_STATE_FULL_WIDTH)) != 0) {
*width = *width * 5 / 4;
}
// Check for format characters enabled
if ((extended->ext_keyboard_mode &
static_cast<uint32_t>(
OrbisImeInitExtKeyboardMode::ENABLE_FORMAT_CHARACTERS)) != 0) {
*height = *height + 30;
}
}
}
// Check for accessibility mode
if ((extended->option & OrbisImeExtOption::ENABLE_ACCESSIBILITY) !=
OrbisImeExtOption::DEFAULT) {
*width = *width * 5 / 4; // 25% larger for accessibility
*height = *height * 5 / 4;
LOG_DEBUG(Lib_ImeDialog, "Accessibility mode: width={}, height={}", *width, *height);
}
// Check for forced accessibility panel
if ((extended->option & OrbisImeExtOption::ACCESSIBILITY_PANEL_FORCED) !=
OrbisImeExtOption::DEFAULT) {
*width = 800;
*height = 600;
LOG_DEBUG(Lib_ImeDialog, "Forced accessibility panel: width={}, height={}", *width,
*height);
}
}
if ((param->option & static_cast<OrbisImeOption>(0x8)) != OrbisImeOption::DEFAULT) { //?
*width *= 2;
*height *= 2;
LOG_DEBUG(Lib_ImeDialog, "Size mode: width={}, height={}", *width, *height);
}
// Adjust for supported languages if specified
if (param->supported_languages != static_cast<OrbisImeLanguage>(0)) {
// Check if CJK languages are supported (need larger panel)
OrbisImeLanguage cjk_mask = OrbisImeLanguage::JAPANESE | OrbisImeLanguage::KOREAN |
OrbisImeLanguage::SIMPLIFIED_CHINESE |
OrbisImeLanguage::TRADITIONAL_CHINESE;
if ((param->supported_languages & cjk_mask) != static_cast<OrbisImeLanguage>(0)) {
*width = *width * 5 / 4; // 25% wider for CJK input
*height = *height * 6 / 5; // 20% taller
LOG_DEBUG(Lib_ImeDialog, "CJK language support: width={}, height={}", *width, *height);
}
// Check if Arabic is supported (right-to-left layout)
if ((param->supported_languages & OrbisImeLanguage::ARABIC) !=
static_cast<OrbisImeLanguage>(0)) {
*width = *width * 11 / 10; // 10% wider for Arabic
LOG_DEBUG(Lib_ImeDialog, "Arabic language support: width={}", *width);
}
}
// Ensure minimum sizes
const uint32_t min_width = 200;
const uint32_t min_height = 100;
if (*width < min_width)
*width = min_width;
if (*height < min_height)
*height = min_height;
// Ensure maximum sizes (don't exceed screen bounds)
bool use_2k_coords =
(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT;
const uint32_t max_width = use_2k_coords ? 2560 : 1920;
const uint32_t max_height = use_2k_coords ? 1440 : 1080;
if (*width > max_width)
*width = max_width;
if (*height > max_height)
*height = max_height;
// Check for fixed position option
if ((param->option & OrbisImeOption::FIXED_POSITION) != OrbisImeOption::DEFAULT) {
if (*width > 800)
*width = 800;
if (*height > 600)
*height = 600;
}
LOG_DEBUG(Lib_ImeDialog, "Final panel size: width={}, height={}", *width, *height);
return Error::OK;
}
Error PS4_SYSV_ABI sceImeDialogGetResult(OrbisImeDialogResult* result) {

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -37,7 +37,9 @@ int PS4_SYSV_ABI sceImeDialogGetCurrentStarState();
int PS4_SYSV_ABI sceImeDialogGetPanelPositionAndForm();
Error PS4_SYSV_ABI sceImeDialogGetPanelSize(const OrbisImeDialogParam* param, u32* width,
u32* height);
int PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended();
Error PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(const OrbisImeDialogParam* param,
const OrbisImeParamExtended* extended,
u32* width, u32* height);
Error PS4_SYSV_ABI sceImeDialogGetResult(OrbisImeDialogResult* result);
OrbisImeDialogStatus PS4_SYSV_ABI sceImeDialogGetStatus();
Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExtended* extended);

View File

@ -655,10 +655,17 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or
switch (file->type) {
case Core::FileSys::FileType::Socket: {
auto native_handle = file->socket->Native();
if (!native_handle) {
// P2P socket, cannot be added to epoll
LOG_ERROR(Lib_Net, "P2P socket cannot be added to epoll (unimplemented)");
*sceNetErrnoLoc() = ORBIS_NET_EBADF;
return ORBIS_NET_ERROR_EBADF;
}
epoll_event native_event = {.events = ConvertEpollEventsIn(event->events),
.data = {.fd = id}};
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_ADD, *file->socket->Native(),
&native_event) == 0);
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_ADD, *native_handle, &native_event) == 0);
epoll->events.emplace_back(id, *event);
break;
}
@ -696,10 +703,17 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or
switch (file->type) {
case Core::FileSys::FileType::Socket: {
auto native_handle = file->socket->Native();
if (!native_handle) {
// P2P socket, cannot be modified in epoll
LOG_ERROR(Lib_Net, "P2P socket cannot be modified in epoll (unimplemented)");
*sceNetErrnoLoc() = ORBIS_NET_EBADF;
return ORBIS_NET_ERROR_EBADF;
}
epoll_event native_event = {.events = ConvertEpollEventsIn(event->events),
.data = {.fd = id}};
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_MOD, *file->socket->Native(),
&native_event) == 0);
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_MOD, *native_handle, &native_event) == 0);
*it = {id, *event};
break;
}
@ -731,8 +745,15 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or
switch (file->type) {
case Core::FileSys::FileType::Socket: {
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_DEL, *file->socket->Native(), nullptr) ==
0);
auto native_handle = file->socket->Native();
if (!native_handle) {
// P2P socket, cannot be removed from epoll
LOG_ERROR(Lib_Net, "P2P socket cannot be removed from epoll (unimplemented)");
*sceNetErrnoLoc() = ORBIS_NET_EBADF;
return ORBIS_NET_ERROR_EBADF;
}
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_DEL, *native_handle, nullptr) == 0);
epoll->events.erase(it);
break;
}

View File

@ -551,6 +551,7 @@ void Emulator::LoadSystemModules(const std::string& game_serial) {
{"libSceJson2.sprx", nullptr},
{"libSceLibcInternal.sprx", &Libraries::LibcInternal::RegisterLib},
{"libSceCesCs.sprx", nullptr},
{"libSceAudiodec.sprx", nullptr},
{"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont},
{"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt},
{"libSceFreeTypeOt.sprx", nullptr}});

View File

@ -70,7 +70,7 @@ void EmulateJoystick(GameController* controller, u32 interval) {
SDL_GetRelativeMouseState(&d_x, &d_y);
float output_speed =
SDL_clamp((sqrt(d_x * d_x + d_y * d_y) + mouse_speed_offset * 128) * mouse_speed,
SDL_clamp(sqrt(d_x * d_x + d_y * d_y) * mouse_speed + mouse_speed_offset * 128,
mouse_deadzone_offset * 128, 128.0);
float angle = atan2(d_y, d_x);

View File

@ -23,6 +23,7 @@
#include <windows.h>
#endif
#include <core/emulator_settings.h>
#include <common/key_manager.h>
int main(int argc, char* argv[]) {
#ifdef _WIN32
@ -38,7 +39,17 @@ int main(int argc, char* argv[]) {
emu_settings->Load();
const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
Config::load(user_dir / "config.toml");
// temp copy the trophy key from old config to key manager if exists
auto key_manager = KeyManager::GetInstance();
if (key_manager->GetAllKeys().TrophyKeySet.ReleaseTrophyKey.empty()) {
if (!Config::getTrophyKey().empty()) {
key_manager->SetAllKeys(
{.TrophyKeySet = {.ReleaseTrophyKey =
KeyManager::HexStringToBytes(Config::getTrophyKey())}});
key_manager->SaveToFile();
}
}
bool has_game_argument = false;
std::string game_path;
std::vector<std::string> game_args{};

View File

@ -153,9 +153,9 @@ std::string NameOf(Attribute attribute) {
case Attribute::TessellationEvaluationPointV:
return "TessellationEvaluationPointV";
case Attribute::PackedHullInvocationInfo:
return "OffChipLdsBase";
case Attribute::OffChipLdsBase:
return "PackedHullInvocationInfo";
case Attribute::OffChipLdsBase:
return "OffChipLdsBase";
case Attribute::TessFactorsBufferBase:
return "TessFactorsBufferBase";
case Attribute::PointSize:

View File

@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <unordered_map>
#include "common/assert.h"
#include "shader_recompiler/info.h"
#include "shader_recompiler/ir/attribute.h"
@ -189,7 +190,7 @@ std::optional<TessSharpLocation> FindTessConstantSharp(IR::Inst* read_const_buff
// Walker that helps deduce what type of attribute a DS instruction is reading
// or writing, which could be an input control point, output control point,
// or per-patch constant (PatchConst).
// For certain ReadConstBuffer instructions using the tess constants V#,, we visit the users
// For certain ReadConstBuffer instructions using the tess constants V#, we visit the users
// recursively and increment a counter on the Load/WriteShared users.
// Namely NumPatch (from m_hsNumPatch), HsOutputBase (m_hsOutputBase),
// and PatchConstBase (m_patchConstBase).
@ -200,9 +201,11 @@ std::optional<TessSharpLocation> FindTessConstantSharp(IR::Inst* read_const_buff
//
// TODO: this will break if AMD compiler used distributive property like
// TcsNumPatches * (ls_stride * #input_cp_in_patch + hs_cp_stride * #output_cp_in_patch)
//
// Assert if the region is ambiguous due to phi nodes in the addr calculation for a DS instruction,
class TessConstantUseWalker {
public:
void MarkTessAttributeUsers(IR::Inst* read_const_buffer, TessConstantAttribute attr) {
void WalkUsersOfTessConstant(IR::Inst* read_const_buffer, TessConstantAttribute attr) {
u32 inc;
switch (attr) {
case TessConstantAttribute::HsNumPatch:
@ -217,14 +220,19 @@ public:
}
for (IR::Use use : read_const_buffer->Uses()) {
MarkTessAttributeUsersHelper(use, inc);
WalkUsersOfTessConstantHelper(use, inc, false);
}
++seq_num;
}
private:
void MarkTessAttributeUsersHelper(IR::Use use, u32 inc) {
struct PhiInfo {
u32 seq_num;
u32 unique_edge;
};
void WalkUsersOfTessConstantHelper(IR::Use use, u32 inc, bool propagateError) {
IR::Inst* inst = use.user;
switch (use.user->GetOpcode()) {
@ -232,38 +240,37 @@ private:
case IR::Opcode::LoadSharedU64:
case IR::Opcode::WriteSharedU32:
case IR::Opcode::WriteSharedU64: {
u32 counter = inst->Flags<u32>();
inst->SetFlags<u32>(counter + inc);
// Stop here
return;
bool is_addr_operand = use.operand == 0;
if (is_addr_operand) {
u32 counter = inst->Flags<u32>();
inst->SetFlags<u32>(counter + inc);
ASSERT_MSG(!propagateError, "LDS instruction {} accesses ambiguous attribute type",
fmt::ptr(use.user));
// Stop here
return;
}
}
case IR::Opcode::Phi: {
struct PhiCounter {
u16 seq_num;
u16 unique_edge;
};
PhiCounter count = inst->Flags<PhiCounter>();
ASSERT_MSG(count.seq_num == 0 || count.unique_edge == use.operand);
auto it = phi_infos.find(use.user);
// the point of seq_num is to tell us if we've already traversed this
// phi on the current walk. Alternatively we could keep a set of phi's
// seen on the current walk. This is to handle phi cycles
if (count.seq_num == 0) {
// phi on the current walk to handle phi cycles
if (it == phi_infos.end()) {
// First time we've encountered this phi
count.seq_num = seq_num;
// Mark the phi as having been traversed originally through this edge
count.unique_edge = use.operand;
} else if (count.seq_num < seq_num) {
count.seq_num = seq_num;
phi_infos[inst] = {.seq_num = seq_num,
.unique_edge = static_cast<u16>(use.operand)};
} else if (it->second.seq_num < seq_num) {
it->second.seq_num = seq_num;
// For now, assume we are visiting this phi via the same edge
// as on other walks. If not, some dataflow analysis might be necessary
ASSERT(count.unique_edge == use.operand);
if (it->second.unique_edge != use.operand) {
propagateError = true;
}
} else {
// count.seq_num == seq_num
ASSERT(it->second.seq_num == seq_num);
// there's a cycle, and we've already been here on this walk
return;
}
inst->SetFlags<PhiCounter>(count);
break;
}
default:
@ -271,10 +278,11 @@ private:
}
for (IR::Use use : inst->Uses()) {
MarkTessAttributeUsersHelper(use, inc);
WalkUsersOfTessConstantHelper(use, inc, propagateError);
}
}
std::unordered_map<const IR::Inst*, PhiInfo> phi_infos;
u32 seq_num{1u};
};
@ -690,7 +698,7 @@ void TessellationPreprocess(IR::Program& program, RuntimeInfo& runtime_info) {
case TessConstantAttribute::HsNumPatch:
case TessConstantAttribute::HsOutputBase:
case TessConstantAttribute::PatchConstBase:
walker.MarkTessAttributeUsers(&inst, tess_const_attr);
walker.WalkUsersOfTessConstant(&inst, tess_const_attr);
// We should be able to safely set these to 0 so that indexing happens only
// within the local patch in the recompiled Vulkan shader. This assumes
// these values only contribute to address calculations for in/out