Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7bcef13
feat: accept extracted PSP game folders in installer
eldaxe Oct 13, 2026
c1c7af2
Add persistent Android log folder picker
eldaxe Oct 13, 2026
eaf7858
Declare Android crash log export descriptor
eldaxe Oct 13, 2026
41ee62e
Call Android crash log folder bridge
eldaxe Oct 13, 2026
6b228db
Write Android crash logs to chosen document folder
eldaxe Oct 13, 2026
dd5f8eb
Write Android crash logs to chosen document folder
eldaxe Oct 13, 2026
eb9dba9
Include Android log bridge declaration
eldaxe Oct 13, 2026
990a446
Fix Android log bridge declaration formatting
eldaxe Oct 13, 2026
19177a1
Fix newline escapes in Android log header
eldaxe Oct 13, 2026
51fad7b
Fix newline escapes in Android log header
eldaxe Oct 13, 2026
120d66a
Merge pull request #2 from eldaxe/android/user-data-folder-logs
eldaxe Oct 13, 2026
bb0c2bc
Implement PSP TLS pool functions
eldaxe Oct 13, 2026
8b1dcc0
Register PSP TLS function NIDs
eldaxe Oct 13, 2026
f0f69d0
Merge pull request #3 from eldaxe/fix/god-eater-tls-startup
eldaxe Oct 13, 2026
8cdf3bc
Merge pull request #1 from eldaxe/feature/extracted-game-folder
eldaxe Oct 13, 2026
dc7dce0
Report Allegrex BREAK as a guest exception with registers
eldaxe Oct 13, 2026
16e6acd
Implement non-blocking PSP lightweight mutex try-lock
eldaxe Oct 13, 2026
2825c48
Register sceKernelTryLockLwMutex_600 NID
eldaxe Oct 13, 2026
0891093
Merge pull request #4 from eldaxe/fix/allegrex-break-diagnostics
eldaxe Oct 13, 2026
731d117
Implement sceFontOpenUserMemory for game startup
eldaxe Oct 13, 2026
64f7b32
Fix sceFontOpenUserMemory error pointer argument
eldaxe Oct 13, 2026
611868f
Mirror extracted-game patch folder to virtual PSP memory stick
eldaxe Oct 13, 2026
42f90fe
Add optional memory-stick patch folder to game profile
eldaxe Oct 13, 2026
2f87e24
Fix patch sync log line endings
eldaxe Oct 13, 2026
22b8041
Implement sceFontSetAltCharacterCode
eldaxe Oct 13, 2026
7dc4537
Name sceFontSetAltCharacterCode NID
eldaxe Oct 13, 2026
c730540
Return EDATA file size from NPDRM size query
eldaxe Oct 13, 2026
07f590e
Fix NPDRM size log line ending
eldaxe Oct 13, 2026
b85f75a
Register sceNpDrmEdataGetDataSize NID
eldaxe Oct 13, 2026
05df8db
Add per-game disc file aliases
eldaxe Oct 13, 2026
eb83167
Resolve memory-stick paths to aliased disc files
eldaxe Oct 13, 2026
558945b
Fix path separator normalization for disc aliases
eldaxe Oct 13, 2026
0300cbb
Log when a disc archive is opened through its memory-stick alias
eldaxe Oct 13, 2026
5eca10b
Fix newline in RDP alias diagnostic
eldaxe Oct 13, 2026
12b845b
Allow raw RDP disc aliases through PGD-key ioctl
eldaxe Oct 13, 2026
b43124d
Add bounded RDP header read diagnostics
eldaxe Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions configs/nids.csv
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Kernel_Library,0x1839852A,sceKernelMemcpy
Kernel_Library,0x1FC64E09,sceKernelLockLwMutexCB
Kernel_Library,0x293B45B8,sceKernelGetThreadId
Kernel_Library,0x5F10D406,sceKernelCpuResumeIntr
Kernel_Library,0x37431849,sceKernelTryLockLwMutex_600
Kernel_Library,0x3B84732D,sceKernelCpuResumeIntrWithSync
Kernel_Library,0xA089ECA4,sceKernelMemset
Kernel_Library,0xB55249D2,sceKernelIsCpuIntrEnable
Expand Down Expand Up @@ -163,6 +164,7 @@ sceLibFont,0x67F17ED7,sceFontNewLib
sceLibFont,0x980F4895,sceFontGetCharGlyphImage
sceLibFont,0xA834319D,sceFontOpen
sceLibFont,0xDCC80C2F,sceFontGetCharInfo
sceLibFont,0xEE232411,sceFontSetAltCharacterCode
sceLibFont,0x02D7F94B,sceFontFlush
sceLibFont,0x57FCB733,sceFontOpenUserFile
sceLibFont,0xBB8E7FE6,sceFontOpenUserMemory
Expand Down Expand Up @@ -566,3 +568,7 @@ sceNetResolver,0x244172AF,sceNetResolverCreate
sceNetResolver,0x224C5F44,sceNetResolverStartNtoA
scePspNpDrm_user,0xA1336091,sceNpDrmSetLicenseeKey
scePspNpDrm_user,0x08D98894,sceNpDrmEdataSetupKey
ThreadManForUser,0x8DAFF657,sceKernelCreateTlspl
ThreadManForUser,0x32BF938E,sceKernelDeleteTlspl
Kernel_Library,0xFA835CDE,sceKernelGetTlsAddr
scePspNpDrm_user,0x219EF5CC,sceNpDrmEdataGetDataSize
10 changes: 10 additions & 0 deletions docs/EXTRACTED_FOLDER_SUPPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Extracted PSP game folders

The installer can accept either an uncompressed PSP ISO or a folder whose top level contains:

- `PSP_GAME/PARAM.SFO`
- `PSP_GAME/SYSDIR/EBOOT.BIN`

It checks the same `DISC_ID` and executable SHA-256 as an ISO install. On desktop platforms the folder is selected from the setup file browser and is used in place. On Android, the system folder picker copies the selected tree into app-private storage first.

The runtime exposes the extracted files through a file-backed virtual disc so ordinary `disc0:/PSP_GAME/...` file operations can use them. This is an initial compatibility path, not a byte-for-byte ISO image: raw disc-sector reads do not see ISO volume descriptors or real ISO directory records. A game that depends on those raw sectors may still require a real ISO. Folder-based boot must be tested per title before claiming compatibility.
43 changes: 41 additions & 2 deletions host/hle/hle_font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@ void blit_glyph(psprecomp::GuestMemory &memory, std::uint32_t image_address, std

} // namespace

std::uint32_t &alternate_character_code() {
static std::uint32_t code = 0u;
return code;
}

void register_font(HleRegistrar &hle) {
fonts::ready();
fonts::set_reload_hook(forget_game_glyphs);
Expand Down Expand Up @@ -303,6 +308,30 @@ void register_font(HleRegistrar &hle) {
if (arg(ctx, 3) != 0u) rt.memory().store32(arg(ctx, 3), 0u);
kernel().finish(ctx, fonts::ready() ? kFontHandle : 0u);
});
// The game embeds a font in guest memory and opens it through this API.
// Glyphs are rendered by the existing host font backend, so the PSP font
// bytes are not parsed here; however, returning the logging-stub value 0
// causes the game's font initialization assertion to fail.
hle.add("sceLibFont", "sceFontOpenUserMemory", [](Runtime &rt, AllegrexContext &ctx) {
const std::uint32_t library = arg(ctx, 0);
const std::uint32_t font_data = arg(ctx, 1);
const std::uint32_t font_size = arg(ctx, 2);
// PSP SDK signature is (library, fontData, fontSize, errorCodePtr):
// the fourth argument is a3, not t0/a fifth pseudo-argument.
const std::uint32_t error_code = arg(ctx, 3);
trace("OpenUserMemory lib=%08X data=%08X size=%08X error=%08X ra=%08X",
library, font_data, font_size, error_code, ctx.gpr[31]);
if (error_code != 0u) rt.memory().store32(error_code, 0u);
kernel().finish(ctx, fonts::ready() && font_data != 0u && font_size != 0u ? kFontHandle : 0u);
});
// The game uses this fallback when a requested glyph is absent from the host font.
hle.add("sceLibFont", "sceFontSetAltCharacterCode", [](Runtime &, AllegrexContext &ctx) {
alternate_character_code() = arg(ctx, 1) & 0xFFFFu;
trace("SetAltCharacterCode lib=%08X code=%04X ra=%08X", arg(ctx, 0),
alternate_character_code(), ctx.gpr[31]);
kernel().finish(ctx, 0u);
});

hle.add("sceLibFont", "sceFontClose", [](Runtime &, AllegrexContext &ctx) {
trace("Close font=%08X", arg(ctx, 0));
kernel().finish(ctx, 0u);
Expand All @@ -322,7 +351,12 @@ void register_font(HleRegistrar &hle) {
kernel().finish(ctx, 0u);
return;
}
const fonts::GlyphMetrics glyph = fonts::ready() ? fonts::metrics(arg(ctx, 1)) : fonts::GlyphMetrics{};
std::uint32_t code = arg(ctx, 1);
fonts::GlyphMetrics glyph = fonts::ready() ? fonts::metrics(code) : fonts::GlyphMetrics{};
if (fonts::ready() && !glyph.found && alternate_character_code() != 0u) {
code = alternate_character_code();
glyph = fonts::metrics(code);
}
write_char_info(rt.memory(), address, glyph);
trace("GetCharInfo code=%04X w=%d h=%d left=%d top=%d adv=%.2f%s ra=%08X", arg(ctx, 1), glyph.width,
glyph.height, glyph.left, glyph.top, static_cast<double>(glyph.advance), glyph.found ? "" : " missing",
Expand All @@ -340,7 +374,12 @@ void register_font(HleRegistrar &hle) {
m.load16(image + 16u), m.load32(image + 20u), ctx.gpr[31]);
}
note_game_cache(rt.memory(), ctx);
if (image != 0u && fonts::ready()) blit_glyph(rt.memory(), image, arg(ctx, 1));
if (image != 0u && fonts::ready()) {
std::uint32_t code = arg(ctx, 1);
if (!fonts::metrics(code).found && alternate_character_code() != 0u)
code = alternate_character_code();
blit_glyph(rt.memory(), image, code);
}
kernel().finish(ctx, 0u);
});
// The same, drawing only inside the rectangle (a3, t0) sized (t1, t2).
Expand Down
95 changes: 94 additions & 1 deletion host/hle/hle_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ struct OpenFile {
std::uint64_t disc_offset{}; // absolute image offset of byte 0
std::uint64_t size{};
std::uint64_t position{};
std::uint32_t alias_diagnostic_reads{};
// The raw disc device counts 2048-byte sectors, not bytes: a seek moves
// to a sector, a read asks for that many sectors and answers with how
// many it got. Everything else here counts bytes, so size and position
Expand Down Expand Up @@ -217,6 +218,31 @@ std::optional<std::pair<std::uint64_t, std::uint64_t>> parse_lbn_path(const std:

std::filesystem::path host_path(const std::string &path) { return io().memory_stick / path; }

std::string normalized_path(std::string path) {
std::replace(path.begin(), path.end(), '\\', '/');
std::transform(path.begin(), path.end(), path.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
while (!path.empty() && path.front() == '/') path.erase(path.begin());
while (!path.empty() && path.back() == '/') path.pop_back();
return path;
}

std::optional<IsoImage::Entry> aliased_disc_file(const std::string &memory_stick_path) {
if (!io().disc) return std::nullopt;
const std::string requested = normalized_path(memory_stick_path);
for (const DiscFileAlias &alias : game().disc_file_aliases) {
if (normalized_path(alias.memory_stick_path) != requested) continue;
const auto entry = io().disc->find(alias.disc_path);
if (entry && !entry->directory) return entry;
}
return std::nullopt;
}

bool is_disc_file_alias(const OpenFile &file) {
const SplitPath split = split_path(file.path);
return split.device == Device::MemoryStick && aliased_disc_file(split.path).has_value();
}

bool trace_io() {
static const bool enabled = portablekit::env("TRACE_IO") != nullptr;
return enabled;
Expand Down Expand Up @@ -255,7 +281,21 @@ std::int64_t open_file(const std::string &full_path, std::uint32_t flags) {
const auto path = host_path(split.path);
std::error_code ec;
const bool exists = std::filesystem::exists(path, ec);
if (!exists && (flags & kOpenCreate) == 0u) return static_cast<std::int32_t>(io_error::kFileNotFound);
if (!exists && (flags & kOpenCreate) == 0u) {
// Some titles store their RDP archives on the UMD but address them
// through an install/save-data path on the memory stick.
if (const auto entry = aliased_disc_file(split.path)) {
file.kind = OpenFile::Kind::Disc;
file.disc_offset = static_cast<std::uint64_t>(entry->lba) * IsoImage::kSectorSize;
file.size = entry->size;
std::cerr << "[io] alias open " << full_path << " -> disc0:/"
<< entry->name << " size=" << entry->size << "\n";
const std::uint32_t fd = io().next_fd++;
io().files.emplace(fd, std::move(file));
return fd;
}
return static_cast<std::int32_t>(io_error::kFileNotFound);
}
std::ios::openmode mode = std::ios::binary | std::ios::in;
if ((flags & kOpenWrite) != 0u) {
std::filesystem::create_directories(path.parent_path(), ec);
Expand Down Expand Up @@ -427,6 +467,27 @@ std::uint32_t read_file_op(Runtime &rt, std::uint32_t fd, std::uint32_t address,
result = static_cast<std::uint32_t>(buffer.size());
}
rt.memory().copy_in(address, buffer);

// GOD EATER 2 asserts while parsing DATA.RDP. Log a bounded sample of
// reads from the disc-backed aliases to reveal offsets, read sizes, and
// the bytes returned to the guest. This does not modify the file data.
if (is_disc_file_alias(file) && file.alias_diagnostic_reads < 8u) {
std::cerr << "[rdp-debug] path=" << file.path
<< " offset=" << file.position
<< " requested=" << requested
<< " got=" << buffer.size()
<< " first_bytes=";
const std::size_t byte_count = std::min<std::size_t>(buffer.size(), 16u);
constexpr char hex[] = "0123456789ABCDEF";
for (std::size_t i = 0; i < byte_count; ++i) {
const unsigned byte = buffer[i];
std::cerr << hex[(byte >> 4u) & 0x0Fu] << hex[byte & 0x0Fu] << ' ';
}
if (buffer.empty()) std::cerr << "<empty>";
std::cerr << '\n';
++file.alias_diagnostic_reads;
}

// A disc read is what tells a load from play (kernel/fast_loading.hpp).
if (file.kind == OpenFile::Kind::Disc) {
load_trace::note_disc_read(buffer.size());
Expand Down Expand Up @@ -566,6 +627,15 @@ std::uint32_t ioctl_op(Runtime &rt, AllegrexContext &ctx) {
std::string error;
auto opened = pgd::PgdFile::open(header, vkey, raw_size, pgd::player_keys(), error);
if (!opened) {
// GOD EATER 2 opens its disc-resident RDP archives through ms0:
// with the PGD-key flag, but these RDP files are not PGD containers.
// Preserve the raw archive view instead of failing the whole open.
if (error == "not a PGD file" && is_disc_file_alias(file)) {
log_once("rdp-raw-" + file.path, "[pgd] " + file.path +
": raw disc archive, skipping PGD decryption");
file.position = 0u;
return 0u;
}
log_once("pgd-" + file.path, "[pgd] " + file.path + ": cannot decrypt: " + error +
" (see docs/DESKTOP_APP.md, \"PGD data\")");
return kPgdUnusable;
Expand Down Expand Up @@ -808,6 +878,9 @@ void register_io(HleRegistrar &hle, const std::filesystem::path &disc_image, con
const bool directory = std::filesystem::is_directory(host, ec);
write_stat(rt.memory(), arg(ctx, 1), directory, directory ? 0u : std::filesystem::file_size(host, ec), 0u);
result = 0u;
} else if (const auto entry = aliased_disc_file(split.path)) {
write_stat(rt.memory(), arg(ctx, 1), false, entry->size, entry->lba);
result = 0u;
}
}
if (trace_io() || result != 0u)
Expand Down Expand Up @@ -934,6 +1007,26 @@ void register_io(HleRegistrar &hle, const std::filesystem::path &disc_image, con
kernel().finish(ctx, ioctl_op(rt, ctx));
});

// sceNpDrmEdataGetDataSize delegates to IoIoctl(0x04100010) on PSP.
// Until EDAT/NPDRM decryption is implemented, report the opened file's
// current size rather than the generated stub's default zero. If a file
// filter has already been installed, file.size is its visible data size.
hle.add("scePspNpDrm_user", "sceNpDrmEdataGetDataSize", [](Runtime &, AllegrexContext &ctx) {
const std::uint32_t fd = arg(ctx, 0);
const auto found = io().files.find(fd);
if (found == io().files.end()) {
kernel().finish(ctx, io_error::kBadFileDescriptor);
return;
}
const OpenFile &file = found->second;
const std::uint32_t size = static_cast<std::uint32_t>(
std::min<std::uint64_t>(file.size, std::numeric_limits<std::uint32_t>::max()));
std::cerr << "[npdrm] sceNpDrmEdataGetDataSize fd=" << fd << " path=" << file.path
<< " size=" << size << (file.filter ? " (filtered)" : " (raw; decryption not applied)") << "\n";
kernel().finish(ctx, size);
});


hle.add("IoFileMgrForUser", "sceIoDevctl", [](Runtime &rt, AllegrexContext &ctx) {
const std::string device = read_cstring(rt.memory(), arg(ctx, 0), 64u);
const std::uint32_t command = arg(ctx, 1);
Expand Down
Loading