From 92571ae1a2daf897e33cab728d8e112a47a27e29 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:14:05 +0300 Subject: [PATCH 01/26] Answer sceKernelIsCpuIntrEnable 1 while interrupts are enabled, 0 while suspended. God of War (UCES00842) asks it at start-up; the logging stub's 0 sent it into an endless loop at 0x08A02EB0 with virtual time frozen at 144 ms. The NID is the first word of SHA-1 of the name. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 1 + host/hle/hle_threadman.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/configs/nids.csv b/configs/nids.csv index 8ffb8a3..9554cbd 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -37,6 +37,7 @@ Kernel_Library,0x293B45B8,sceKernelGetThreadId Kernel_Library,0x5F10D406,sceKernelCpuResumeIntr Kernel_Library,0x3B84732D,sceKernelCpuResumeIntrWithSync Kernel_Library,0xA089ECA4,sceKernelMemset +Kernel_Library,0xB55249D2,sceKernelIsCpuIntrEnable Kernel_Library,0xBEA46419,sceKernelLockLwMutex Kernel_Library,0xD13BDE95,sceKernelCheckThreadStack LoadExecForUser,0x05572A5F,sceKernelExitGame diff --git a/host/hle/hle_threadman.cpp b/host/hle/hle_threadman.cpp index 7e5507e..d8c2381 100644 --- a/host/hle/hle_threadman.cpp +++ b/host/hle/hle_threadman.cpp @@ -875,6 +875,12 @@ void register_kernel_library(HleRegistrar &hle) { kernel().finish(ctx, 0u); }; hle.add("Kernel_Library", "sceKernelCpuResumeIntr", resume_interrupts); + // sceKernelIsCpuIntrEnable() -> 1 while interrupts are enabled. God of + // War (UCES00842) asks it at start-up and, given 0, stops in a loop that + // never ends. + hle.add("Kernel_Library", "sceKernelIsCpuIntrEnable", [](Runtime &, AllegrexContext &ctx) { + kernel().finish(ctx, kernel().interrupts_enabled() ? 1u : 0u); + }); // With a pipeline sync on a PSP, which has nothing to wait for here. hle.add("Kernel_Library", "sceKernelCpuResumeIntrWithSync", resume_interrupts); hle.add("Kernel_Library", "sceKernelMemset", [](Runtime &rt, AllegrexContext &ctx) { From 40a93847b4333280ef0eab38e0db27f1a1301488 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:14:17 +0300 Subject: [PATCH 02/26] List disc directories with the names as the disc spells them Directory listings (sceIoDread) returned every name upper-cased, the form lookups are normalised to. God of War (UCES00842) builds a table of Jenkins hashes of the full paths it reads from the disc's directories and looks files up by its own mixed-case paths (data/English/game.bin); with upper-cased names nothing matched, its localisation file was never loaded and it crashed on a null pointer at 0x089E7CF0. Lookups stay case-insensitive. Co-Authored-By: Claude Opus 5.5 (1M context) --- host/kernel/iso_image.cpp | 5 ++--- host/kernel/iso_image.hpp | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/host/kernel/iso_image.cpp b/host/kernel/iso_image.cpp index 25bf535..a0272e2 100644 --- a/host/kernel/iso_image.cpp +++ b/host/kernel/iso_image.cpp @@ -60,7 +60,7 @@ void IsoImage::scan_directory(std::uint32_t lba, std::uint32_t size, const std:: if (name_length == 1u && (name[0] == '\0' || name[0] == '\1')) continue; if (const auto version = name.find(';'); version != std::string::npos) name.resize(version); const std::string full = normalize(prefix.empty() ? name : prefix + "/" + name); - entries_[full] = Entry{entry_lba, entry_size, directory}; + entries_[full] = Entry{entry_lba, entry_size, directory, name}; if (directory) scan_directory(entry_lba, entry_size, full, depth + 1); } } @@ -84,11 +84,10 @@ std::vector IsoImage::list(std::string directory) const { const std::string prefix = normalize(std::move(directory)); std::vector names; for (const auto &[path, entry] : entries_) { - (void)entry; if (path.empty() || path.size() <= prefix.size()) continue; if (!prefix.empty() && (path.compare(0, prefix.size(), prefix) != 0 || path[prefix.size()] != '/')) continue; const std::string rest = prefix.empty() ? path : path.substr(prefix.size() + 1u); - if (rest.find('/') == std::string::npos) names.push_back(rest); + if (rest.find('/') == std::string::npos) names.push_back(entry.name.empty() ? rest : entry.name); } return names; } diff --git a/host/kernel/iso_image.hpp b/host/kernel/iso_image.hpp index 152e3ed..e52d6a9 100644 --- a/host/kernel/iso_image.hpp +++ b/host/kernel/iso_image.hpp @@ -13,6 +13,9 @@ namespace portablekit { // Read-only ISO9660 view of a PSP UMD image. Paths are case-insensitive and // use '/' separators relative to the image root (e.g. "PSP_GAME/USRDIR/DATA.BIN"). +// Listings keep each name as the disc spells it: a game may hash the names +// it reads from a directory (God of War does) and look them up by its own +// mixed-case paths. class IsoImage { public: static constexpr std::uint32_t kSectorSize = 2048u; @@ -21,6 +24,7 @@ class IsoImage { std::uint32_t lba{}; std::uint32_t size{}; bool directory{}; + std::string name; // the last component, as on the disc }; explicit IsoImage(const std::filesystem::path &path); From 9a2521b93f5da468b8524137a430178fff01a7c2 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:14:17 +0300 Subject: [PATCH 03/26] Implement sceIoChdir and open disc files with write access read-only sceIoChdir sets the directory that paths without a device are relative to; God of War sets disc0:/PSP_GAME/USRDIR/ at start-up. It opens its disc archives with flags 0x3 (read-write) and cannot start without them, so a disc open no longer fails on write access; writes to disc files still fail. _TRACE_IO shows each directory entry's sector and size. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 1 + host/hle/hle_io.cpp | 44 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/configs/nids.csv b/configs/nids.csv index 9554cbd..5f6b210 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -29,6 +29,7 @@ IoFileMgrForUser,0xB29DDF9C,sceIoDopen IoFileMgrForUser,0xE3EB004C,sceIoDread IoFileMgrForUser,0xEB092469,sceIoDclose IoFileMgrForUser,0xB8A740F4,sceIoChstat +IoFileMgrForUser,0x55F4717D,sceIoChdir Kernel_Library,0x092968F4,sceKernelCpuSuspendIntr Kernel_Library,0x15B6446B,sceKernelUnlockLwMutex Kernel_Library,0x1839852A,sceKernelMemcpy diff --git a/host/hle/hle_io.cpp b/host/hle/hle_io.cpp index 6388bb7..165ba5a 100644 --- a/host/hle/hle_io.cpp +++ b/host/hle/hle_io.cpp @@ -116,6 +116,9 @@ struct IoState { std::uint32_t next_fd{3u}; std::map read_ahead; std::uint32_t next_read_ahead{1u}; + // sceIoChdir's directory ("device:/path"), which a path without a device + // is relative to; empty until a game sets one, and then the disc's root. + std::string current_directory; }; IoState &io() { @@ -143,8 +146,16 @@ struct SplitPath { std::string path; // without device, '/' separated, no leading '/' }; -SplitPath split_path(const std::string &full) { +SplitPath split_path(const std::string &given) { SplitPath result; + std::string full = given; + if (full.find(':') == std::string::npos && !io().current_directory.empty()) { + const std::string ¤t = io().current_directory; + if (!full.empty() && (full[0] == '/' || full[0] == '\\')) + full = current.substr(0, current.find(':') + 1u) + full; + else + full = current + "/" + full; + } const auto colon = full.find(':'); std::string device = colon == std::string::npos ? "disc0" : full.substr(0, colon); std::transform(device.begin(), device.end(), device.begin(), [](unsigned char c) { return std::tolower(c); }); @@ -208,7 +219,9 @@ std::int64_t open_file(const std::string &full_path, std::uint32_t flags) { file.path = full_path; if (split.device == Device::Disc) { if (!io().disc) return static_cast(io_error::kDeviceNotFound); - if ((flags & kOpenWrite) != 0u) return static_cast(io_error::kReadOnly); + // Write access is not refused at the open: God of War (UCES00842) + // opens its disc archives read-write (flags 0x3) and cannot go on + // without them. Writes to a disc file are what fail. if (split.path.empty()) { // The device itself, with no path: the whole image as a stream of // sectors, which is how a game reads the disc without going @@ -653,6 +666,29 @@ void register_io(HleRegistrar &hle, const std::filesystem::path &disc_image, con kernel().finish64(ctx, static_cast(lseek_op(arg(ctx, 0), static_cast(arg64(ctx, 2)), arg(ctx, 4)))); }); register_async_io(hle); + // sceIoChdir(path): the directory later paths without a device are + // relative to. God of War (UCES00842) sets it at start-up. + hle.add("IoFileMgrForUser", "sceIoChdir", [](Runtime &rt, AllegrexContext &ctx) { + const std::string path = read_cstring(rt.memory(), arg(ctx, 0), 256u); + const SplitPath split = split_path(path); + std::uint32_t result = 0u; + if (split.device == Device::Disc) { + std::optional entry; + if (io().disc) entry = io().disc->find(split.path); + if (!split.path.empty() && (!entry || !entry->directory)) result = io_error::kFileNotFound; + } else if (split.device == Device::MemoryStick) { + std::error_code ec; + if (!std::filesystem::is_directory(host_path(split.path), ec)) result = io_error::kFileNotFound; + } else { + result = io_error::kDeviceNotFound; + } + if (result == 0u) { + const std::string device = split.device == Device::Disc ? "disc0:" : "ms0:"; + io().current_directory = device + "/" + split.path; + } + if (trace_io()) std::cerr << "[io] chdir " << path << " -> " << psprecomp::hex32(result) << "\n"; + kernel().finish(ctx, result); + }); hle.add("IoFileMgrForUser", "sceIoGetstat", [](Runtime &rt, AllegrexContext &ctx) { const std::string path = read_cstring(rt.memory(), arg(ctx, 0), 256u); const SplitPath split = split_path(path); @@ -740,7 +776,9 @@ void register_io(HleRegistrar &hle, const std::filesystem::path &disc_image, con write_stat(memory, dirent, entry.directory, entry.size, entry.lba); for (std::uint32_t i = 0; i < 256u; ++i) memory.store8(dirent + 88u + i, i < entry.name.size() ? static_cast(entry.name[i]) : 0u); - if (trace_io()) std::cerr << "[io] dread " << directory.path << " -> " << entry.name << "\n"; + if (trace_io()) + std::cerr << "[io] dread " << directory.path << " -> " << entry.name << " lba=" << entry.lba + << " size=" << entry.size << "\n"; kernel().finish(ctx, 1u); }); hle.add("IoFileMgrForUser", "sceIoDclose", [](Runtime &, AllegrexContext &ctx) { From 8520ac1d0197e19cd8b943540b7d1f023acce37d Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:25:31 +0300 Subject: [PATCH 04/26] Ask for a looping streamed ATRAC track's loop again once the file is in A streamed track that loops kept reporting all its data on memory once the whole file had been added, and dropped the bytes it had decoded; after the first pass every frame of the loop decoded silent. Patapon's title music (UCES00995) went quiet after about 30 seconds. Once the file end has been added, the next data is asked for from the frame the loop starts in (sceAtracGetStreamDataInfo's read offset), the remaining-frame count follows the loop, and the bytes from the loop start on are kept, so the loop decodes from what was added the first time whenever the game refills. Traced: Patapon now adds the loop again (next=229584...) and no frame decodes silent in a 100 s run. Co-Authored-By: Claude Opus 5.5 (1M context) --- host/hle/hle_atrac.cpp | 79 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/host/hle/hle_atrac.cpp b/host/hle/hle_atrac.cpp index f275cf0..911900a 100644 --- a/host/hle/hle_atrac.cpp +++ b/host/hle/hle_atrac.cpp @@ -100,6 +100,13 @@ struct AtracContext { // to tell the game where to write next. bool streaming{}; std::uint32_t written{}; // bytes of the file delivered, from its start + // Where the game adds data next. It follows `written` until the whole + // file has been added; a track that loops then asks again from the frame + // the loop starts in, as many times as it loops (`wraps`), while the + // decoder has gone round the loop `loops_done` times. + std::uint32_t cursor{}; + std::uint32_t wraps{}; + std::uint32_t loops_done{}; std::uint32_t stored_from{}; // file offset of stored[0] std::vector stored; @@ -213,6 +220,21 @@ bool looping(const AtracContext &context) { return context.track.loop_start >= 0 && context.loop_num != 0; } +// The file offset of the frame the loop starts in. +std::uint32_t loop_offset(const AtracContext &context) { + const TrackInfo &track = context.track; + const auto frame_samples = static_cast(audio::atrac_frame_samples(track.codec)); + const std::int64_t frame = (static_cast(std::max(track.loop_start, 0)) + track.skip) / frame_samples; + return track.data_offset + static_cast(frame) * track.block_align; +} + +// A file offset as a position in the order the track plays: each pass round +// the loop adds the loop's length. +std::uint64_t play_order(const AtracContext &context, std::uint32_t offset, std::uint32_t passes) { + const std::uint32_t loop_bytes = context.track.file_size - std::min(loop_offset(context), context.track.file_size); + return offset + static_cast(passes) * loop_bytes; +} + // Decodes stream frame `frame` into the cache, reseeding the decoder when the // request is not the frame that follows the last one it decoded. bool load_frame(const psprecomp::GuestMemory &memory, AtracContext &context, std::int64_t frame) { @@ -267,7 +289,13 @@ std::uint32_t current_frame_offset(const AtracContext &context) { void trim_stored(AtracContext &context) { const std::uint32_t keep_from_wanted = current_frame_offset(context); const std::uint32_t warm_up = 2u * context.track.block_align; - const std::uint32_t keep_from = keep_from_wanted > warm_up ? keep_from_wanted - warm_up : 0u; + std::uint32_t keep_from = keep_from_wanted > warm_up ? keep_from_wanted - warm_up : 0u; + // A track that loops decodes its loop again from these bytes: the game + // is asked for them again too, but what it adds then is not needed. + if (context.track.loop_start >= 0) { + const std::uint32_t loop_from = loop_offset(context); + keep_from = std::min(keep_from, loop_from > warm_up ? loop_from - warm_up : 0u); + } if (keep_from <= context.stored_from) return; const std::uint32_t drop = std::min(keep_from - context.stored_from, static_cast(context.stored.size())); @@ -279,20 +307,28 @@ void trim_stored(AtracContext &context) { // bytes from `written` on. void store_added(const psprecomp::GuestMemory &memory, AtracContext &context, std::uint32_t address, std::uint32_t size) { - if (context.stored.empty()) context.stored_from = context.written; - const std::size_t at = context.stored.size(); - context.stored.resize(at + size); - memory.copy_out(address, std::span(context.stored).subspan(at, size)); - context.written += size; + if (context.cursor >= context.written) { + if (context.stored.empty()) context.stored_from = context.written; + const std::size_t at = context.stored.size(); + context.stored.resize(at + size); + memory.copy_out(address, std::span(context.stored).subspan(at, size)); + context.written += size; + } + context.cursor += size; + if (context.cursor >= context.track.file_size && looping(context)) { + context.cursor = loop_offset(context); + ++context.wraps; + } } // Frames in the buffer the game has not decoded yet, or "all of it" once the // whole file has been delivered. std::int32_t remain_frames(const AtracContext &context) { - if (!context.streaming || context.written >= context.track.file_size) return kRemainAllDataOnMemory; - const std::uint32_t from = current_frame_offset(context); - if (context.written <= from) return 0; - return static_cast((context.written - from) / context.track.block_align); + if (!context.streaming || context.cursor >= context.track.file_size) return kRemainAllDataOnMemory; + const std::uint64_t delivered = play_order(context, context.cursor, context.wraps); + const std::uint64_t from = play_order(context, current_frame_offset(context), context.loops_done); + if (delivered <= from) return 0; + return static_cast((delivered - from) / context.track.block_align); } // Where the game writes next, how much fits without overwriting a byte not @@ -330,11 +366,13 @@ std::uint32_t ring_address(const AtracContext &context, std::uint32_t offset) { StreamWrite stream_write(const AtracContext &context) { const TrackInfo &track = context.track; const std::uint32_t frames = ring_frames(context); - if (frames == 0u) return {context.buffer, 0u, context.written}; + if (frames == 0u) return {context.buffer, 0u, context.cursor}; const std::uint32_t ring = frames * track.block_align; - const std::uint32_t written = std::max(context.written, track.data_offset); - const std::uint32_t pending = written - std::min(written, current_frame_offset(context)); - std::uint32_t writable = pending < ring ? ring - pending : 0u; + const std::uint32_t written = std::max(context.cursor, track.data_offset); + const std::uint64_t delivered = play_order(context, written, context.wraps); + const std::uint64_t playing = play_order(context, current_frame_offset(context), context.loops_done); + const std::uint64_t pending = delivered - std::min(delivered, playing); + std::uint32_t writable = pending < ring ? ring - static_cast(pending) : 0u; // Contiguous: up to the end of the last slot. const std::uint32_t into_ring = (written - track.data_offset) % ring; writable = std::min(writable, ring - into_ring); @@ -589,6 +627,7 @@ void register_atrac_functions(HleRegistrar &hle) { context->position += count; if (context->position > stop && loops) { context->position = track.loop_start; + ++context->loops_done; if (context->loop_num > 0) --context->loop_num; } const bool ended = context->position > track.end_sample; @@ -662,6 +701,12 @@ void register_atrac_functions(HleRegistrar &hle) { return; } context->loop_num = static_cast(arg(ctx, 1)); + // Loops set after the whole file was added: the next data the game + // adds is the loop's again. + if (context->streaming && context->cursor >= context->track.file_size && looping(*context)) { + context->cursor = loop_offset(*context); + ++context->wraps; + } finish_traced(ctx, "sceAtracSetLoopNum", 0u); }); @@ -736,7 +781,8 @@ void register_atrac_functions(HleRegistrar &hle) { return; } store_added(rt.memory(), *context, write.address, added); - finish_traced(ctx, "sceAtracAddStreamData", 0u, "written=" + std::to_string(context->written)); + finish_traced(ctx, "sceAtracAddStreamData", 0u, + "written=" + std::to_string(context->written) + " next=" + std::to_string(context->cursor)); }); // sceAtracGetNextSample(id, outSamples): how many samples the next // DecodeData returns. @@ -820,6 +866,9 @@ void register_atrac_functions(HleRegistrar &hle) { const std::uint32_t address = ring_address(*context, from); context->stored.clear(); context->written = from; + context->cursor = from; + context->wraps = 0u; + context->loops_done = 0u; store_added(rt.memory(), *context, address, added); context->cached_frame = -1; context->next_frame = -1; From 782d945f7238e09c20d1dc7cbbb51f2e24a828a7 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:50:13 +0300 Subject: [PATCH 05/26] Number the GE's own SIGNAL list flow as Patapon uses it beab5af read SIGNAL behaviour 0x11 as a jump and 0x12 as a call. The title screen shows otherwise: each frame's list reaches a list inside DATA_CMN.BND with 0x0E1108F5, and that list ends in 0x0E120000, after which the frame list goes on with the title menu. 0x11 is the call, 0x12 the return, and 0x10 the jump. Read as before, the return was taken for a call to an address below RAM, the list stopped there, and the title had no menu: NEW GAME / CONTINUE / LANGUAGE now draw and respond. Co-Authored-By: Claude Opus 5.5 (1M context) --- host/gpu/ge_state.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/host/gpu/ge_state.cpp b/host/gpu/ge_state.cpp index 8fb164f..48af218 100644 --- a/host/gpu/ge_state.cpp +++ b/host/gpu/ge_state.cpp @@ -1419,31 +1419,30 @@ std::uint32_t GeState::execute(const GuestMemory &memory, std::uint32_t pc, std: // SIGNAL's top byte is its behaviour. 0x01-0x03 interrupt the CPU // with the low 16 bits; 0x10 and up are list flow the GE does // itself, with a target address split between this command - // (high half) and the END that follows (low half). Patapon's - // frame list ends in behaviour 0x11 into a list inside a loaded - // archive (0x0E1108ED, 0x0C11CAC0: to 0x08EDCAC0), which is - // where its screens are. + // (high half) and the END that follows (low half): 0x10 jumps, + // 0x11 calls, 0x12 returns. Traced in Patapon (UCES00995): each + // frame's list calls, with 0x0E1108F5 and an END ending FD40, a list inside + // a loaded archive, which ends in 0x0E120000 and the frame list + // goes on after the call with the title menu. const std::uint32_t behaviour = data >> 16u; - if (behaviour >= 0x10u && behaviour <= 0x13u && memory.contains(pc, 4u) && + if (behaviour >= 0x10u && behaviour <= 0x12u && memory.contains(pc, 4u) && (memory.load32(pc) >> 24u) == kEnd) { const std::uint32_t target = ((data & 0xFFFFu) << 16u | (memory.load32(pc) & 0xFFFFu)) & 0x0FFFFFFCu; pc += 4u; // the END is part of the signal switch (behaviour) { - case 0x11u: // jump + case 0x10u: // jump pc = target; break; - case 0x12u: // call + case 0x11u: // call call_stack_.push_back(pc); pc = target; break; - case 0x13u: // return + default: // 0x12: return if (!call_stack_.empty()) { pc = call_stack_.back(); call_stack_.pop_back(); } break; - default: // 0x10: a sync point - break; } continue; } From 13ba7fa0003e879db3a24952348c85e903009c6a Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:50:13 +0300 Subject: [PATCH 06/26] Add _TRACE_SAS: voices set, keyed on and off, end flags Each __sceSasSetVoice, __sceSasSetKeyOn and __sceSasSetKeyOff, and each change of the end flags a game polls; used to see that Patapon's title answered Cross with a sound when its menu did not draw. Co-Authored-By: Claude Opus 5.5 (1M context) --- host/hle/hle_media.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/host/hle/hle_media.cpp b/host/hle/hle_media.cpp index 4a1f9ff..36f18ff 100644 --- a/host/hle/hle_media.cpp +++ b/host/hle/hle_media.cpp @@ -807,6 +807,13 @@ void sas_render(Runtime &rt, std::uint32_t core, std::uint32_t output, bool mix, } } +// _TRACE_SAS: voices set, keyed on and off, and each change of the +// end flags the game polls. +bool trace_sas() { + static const bool enabled = portablekit::env("TRACE_SAS") != nullptr; + return enabled; +} + void register_audio(HleRegistrar &hle) { audio::AudioSink::instance().initialize(); @@ -921,6 +928,9 @@ void register_audio(HleRegistrar &hle) { kernel().finish(ctx, 0u); }); hle.add("sceSasCore", "__sceSasSetVoice", [](Runtime &, AllegrexContext &ctx) { + if (trace_sas()) + std::cerr << "[sas] voice " << arg(ctx, 1) << " vag=" << psprecomp::hex32(arg(ctx, 2)) << " size=" << arg(ctx, 3) + << " loop=" << arg(ctx, 4) << "\n"; audio::sas_core(arg(ctx, 0)).set_voice(arg(ctx, 1), arg(ctx, 2), arg(ctx, 3), arg(ctx, 4) != 0u); kernel().finish(ctx, 0u); }); @@ -951,10 +961,12 @@ void register_audio(HleRegistrar &hle) { kernel().finish(ctx, 0u); }); hle.add("sceSasCore", "__sceSasSetKeyOn", [](Runtime &, AllegrexContext &ctx) { + if (trace_sas()) std::cerr << "[sas] key on " << arg(ctx, 1) << "\n"; audio::sas_core(arg(ctx, 0)).key_on(arg(ctx, 1)); kernel().finish(ctx, 0u); }); hle.add("sceSasCore", "__sceSasSetKeyOff", [](Runtime &, AllegrexContext &ctx) { + if (trace_sas()) std::cerr << "[sas] key off " << arg(ctx, 1) << "\n"; audio::sas_core(arg(ctx, 0)).key_off(arg(ctx, 1)); kernel().finish(ctx, 0u); }); @@ -1021,7 +1033,11 @@ void register_audio(HleRegistrar &hle) { kernel().finish(ctx, audio::sas_core(arg(ctx, 0)).output_mode()); }); hle.add("sceSasCore", "__sceSasGetEndFlag", [](Runtime &, AllegrexContext &ctx) { - kernel().finish(ctx, audio::sas_core(arg(ctx, 0)).end_flag()); + const std::uint32_t flags = audio::sas_core(arg(ctx, 0)).end_flag(); + static std::uint32_t last = 0u; + if (trace_sas() && flags != last) std::cerr << "[sas] end flags " << psprecomp::hex32(flags) << "\n"; + last = flags; + kernel().finish(ctx, flags); }); hle.add("sceSasCore", "__sceSasCore", [](Runtime &rt, AllegrexContext &ctx) { sas_render(rt, arg(ctx, 0), arg(ctx, 1), false, 0u, 0u); From dd7276d18049b5c7ddcf2d3b0b27fd7687d45456 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:22:02 +0300 Subject: [PATCH 07/26] Implement message pipes (sceKernel*MsgPipe) Create, Delete, Send/SendCB/TrySend, Receive/ReceiveCB/TryReceive, Cancel and ReferStatus. A pipe is a byte stream through a buffer of partition memory; sends and receives wait for all of their bytes (wait mode 0) or for any (1), first come first served, with timeouts, and a pipe with no buffer hands bytes straight to a waiting receiver. The bookkeeping is in kernel/message_pipe.* with unit tests. Patapon (UCES00995) creates a pipe of 1024 bytes when a new game starts, sends 0x54-byte messages to a worker thread that polls with TryReceiveMsgPipe; with the stubs the worker never heard and New Game did nothing. Traced there: Create, Send, TryReceive. Cancel and ReferStatus say UNVERIFIED on first use. NIDs match SHA-1 of the names; the error codes follow the kernel's numbering (UNKNOWN_MPPID 0x8002019E, MPP_FULL 0x800201B3, MPP_EMPTY 0x800201B4). Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 10 ++ cmake/PortableKit.cmake | 1 + configs/nids.csv | 10 ++ host/hle/hle_threadman.cpp | 211 +++++++++++++++++++++++++++++++++++ host/kernel/message_pipe.cpp | 160 ++++++++++++++++++++++++++ host/kernel/message_pipe.hpp | 94 ++++++++++++++++ tests/pipe_tests.cpp | 91 +++++++++++++++ 7 files changed, 577 insertions(+) create mode 100644 host/kernel/message_pipe.cpp create mode 100644 host/kernel/message_pipe.hpp create mode 100644 tests/pipe_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cfa418..ea55481 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -234,6 +234,16 @@ if(PSPRECOMP_BUILD_TESTS) add_test(NAME portablekit_pool_tests COMMAND portablekit_pool_tests) endif() +# Message pipes (sceKernel*MsgPipe): their bookkeeping, with no kernel. +if(PSPRECOMP_BUILD_TESTS) + add_executable(portablekit_pipe_tests + "${PORTABLEKIT_ROOT}/tests/pipe_tests.cpp" + "${PORTABLEKIT_ROOT}/host/kernel/message_pipe.cpp") + target_include_directories(portablekit_pipe_tests PRIVATE "${PORTABLEKIT_ROOT}/host") + set_target_properties(portablekit_pipe_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + add_test(NAME portablekit_pipe_tests COMMAND portablekit_pipe_tests) +endif() + # Each platform's settings defaults. if(PSPRECOMP_BUILD_TESTS) add_executable(portablekit_settings_tests diff --git a/cmake/PortableKit.cmake b/cmake/PortableKit.cmake index a5ab095..8e36e13 100644 --- a/cmake/PortableKit.cmake +++ b/cmake/PortableKit.cmake @@ -45,6 +45,7 @@ set(PORTABLEKIT_HOST_SOURCES host/system.cpp host/kernel/kernel.cpp host/kernel/fixed_pool.cpp + host/kernel/message_pipe.cpp host/kernel/load_trace.cpp host/kernel/fast_loading.cpp host/kernel/load_detector.cpp diff --git a/configs/nids.csv b/configs/nids.csv index 5f6b210..7dd5045 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -443,6 +443,16 @@ ThreadManForUser,0xB736E9FF,sceKernelFreeVpl ThreadManForUser,0xEC0A693F,sceKernelAllocateVplCB ThreadManForUser,0xBED27435,sceKernelAllocateVpl ThreadManForUser,0xAF36D708,sceKernelTryAllocateVpl +ThreadManForUser,0x7C0DC2A0,sceKernelCreateMsgPipe +ThreadManForUser,0xF0B7DA1C,sceKernelDeleteMsgPipe +ThreadManForUser,0x876DBFAD,sceKernelSendMsgPipe +ThreadManForUser,0x7C41F2C2,sceKernelSendMsgPipeCB +ThreadManForUser,0x884C9F90,sceKernelTrySendMsgPipe +ThreadManForUser,0x74829B76,sceKernelReceiveMsgPipe +ThreadManForUser,0xFBFA697D,sceKernelReceiveMsgPipeCB +ThreadManForUser,0xDF52098F,sceKernelTryReceiveMsgPipe +ThreadManForUser,0x349B864D,sceKernelCancelMsgPipe +ThreadManForUser,0x33BE4024,sceKernelReferMsgPipeStatus UtilsForUser,0x27CC57F0,sceKernelLibcTime UtilsForUser,0x3EE30821,sceKernelDcacheWritebackRange UtilsForUser,0x6AD345D7,sceKernelSetGPO diff --git a/host/hle/hle_threadman.cpp b/host/hle/hle_threadman.cpp index d8c2381..987c94e 100644 --- a/host/hle/hle_threadman.cpp +++ b/host/hle/hle_threadman.cpp @@ -3,6 +3,7 @@ #include "hle_common.hpp" #include "kernel/fixed_pool.hpp" +#include "kernel/message_pipe.hpp" #include "psprecomp/common.hpp" @@ -1311,6 +1312,215 @@ void register_fpls(HleRegistrar &hle) { }); } + +// Message pipes: a byte stream between threads, through a buffer of partition +// memory (kernel/message_pipe.hpp keeps the books; the bytes themselves are +// kept on the host). The calls follow the SDK's pspthreadman.h and uOFW's +// names for the arguments it leaves unnamed: send and receive take (uid, +// message, size, wait mode, int *result, unsigned *timeout), wait mode 0 for +// all of `size` and 1 for as much as there is, and *result gets the byte +// count. Patapon (UCES00995) passes messages to a worker thread this way when +// a new game starts. +constexpr std::uint32_t kUnknownMppid = 0x8002019Eu; +constexpr std::uint32_t kMppFull = 0x800201B3u; +constexpr std::uint32_t kMppEmpty = 0x800201B4u; +constexpr std::uint32_t kMppAttrHighMemory = 0x4000u; + +struct MsgPipe { + std::string name; + std::uint32_t attributes{}; + SceUID block{-1}; + kernel_pipes::MessagePipe pipe; +}; + +std::map &msg_pipes() { + static std::map pipes; + return pipes; +} + +std::vector read_bytes(const psprecomp::GuestMemory &memory, std::uint32_t address, std::uint32_t size) { + std::vector bytes(size); + if (size != 0u && memory.contains(address, size)) memory.copy_out(address, bytes); + return bytes; +} + +void write_bytes(psprecomp::GuestMemory &memory, std::uint32_t address, const std::vector &bytes) { + for (std::size_t i = 0; i < bytes.size(); ++i) memory.store8(address + static_cast(i), bytes[i]); +} + +void register_msg_pipes(HleRegistrar &hle) { + // sceKernelCreateMsgPipe(name, partition, attr, buffer size, option) + hle.add("ThreadManForUser", "sceKernelCreateMsgPipe", [](Runtime &rt, AllegrexContext &ctx) { + MsgPipe pipe; + pipe.name = read_cstring(rt.memory(), arg(ctx, 0), 32u); + pipe.attributes = arg(ctx, 2); + const std::uint32_t size = arg(ctx, 3); + if (size != 0u) { + const std::uint32_t type = (pipe.attributes & kMppAttrHighMemory) != 0u ? 1u : 0u; + pipe.block = kernel().allocate_block("MsgPipe", type, size, 0u); + if (pipe.block < 0) { + kernel().finish(ctx, error::kNoMemory); + return; + } + } + pipe.pipe = kernel_pipes::MessagePipe(size); + if (trace_sync()) + log_sync("[kernel] CreateMsgPipe \"" + pipe.name + "\" size=" + std::to_string(size) + + " attr=" + psprecomp::hex32(pipe.attributes)); + const SceUID uid = kernel().allocate_uid(); + msg_pipes()[uid] = std::move(pipe); + kernel().finish(ctx, as_unsigned(uid)); + }); + hle.add("ThreadManForUser", "sceKernelDeleteMsgPipe", [](Runtime &, AllegrexContext &ctx) { + const auto found = msg_pipes().find(as_signed(arg(ctx, 0))); + if (found == msg_pipes().end()) { + kernel().finish(ctx, kUnknownMppid); + return; + } + if (found->second.block >= 0) (void)kernel().free_block(found->second.block); + msg_pipes().erase(found); // waiters see it gone and end with WAIT_DELETE + kernel().finish(ctx, 0u); + }); + + const auto send = [](bool may_wait) { + return [may_wait](Runtime &rt, AllegrexContext &ctx) { + const SceUID uid = as_signed(arg(ctx, 0)); + const auto found = msg_pipes().find(uid); + if (found == msg_pipes().end()) { + kernel().finish(ctx, kUnknownMppid); + return; + } + auto &memory = rt.memory(); + const std::uint32_t size = arg(ctx, 2); + const bool whole = arg(ctx, 3) == 0u; + const std::uint32_t result_address = ctx.gpr[8]; // t0 + const std::uint32_t timeout_address = ctx.gpr[9]; // t1 + std::vector bytes = read_bytes(memory, arg(ctx, 1), size); + kernel_pipes::MessagePipe &pipe = found->second.pipe; + if (!may_wait) { + const auto sent = pipe.try_send(bytes, whole); + if (result_address != 0u && sent) memory.store32(result_address, *sent); + kernel().finish(ctx, sent ? 0u : kMppFull); + return; + } + const auto ticket = pipe.send(std::move(bytes), whole); + const std::uint64_t generation = pipe.cancel_generation(); + std::optional timeout; + if (timeout_address != 0u) timeout = memory.load32(timeout_address); + kernel().wait_host(ctx, timeout, [uid, ticket, generation, result_address, timeout_address, &memory]( + bool timed_out) -> std::optional { + const auto pipe = msg_pipes().find(uid); + if (pipe == msg_pipes().end()) return error::kWaitDelete; + kernel_pipes::MessagePipe &waited = pipe->second.pipe; + if (waited.cancel_generation() != generation) return kWaitCancel; + if (const auto sent = waited.sent(ticket)) { + if (result_address != 0u) memory.store32(result_address, *sent); + return 0u; + } + if (!timed_out) return std::nullopt; + const std::uint32_t sent = waited.withdraw(ticket); + if (result_address != 0u) memory.store32(result_address, sent); + if (timeout_address != 0u) memory.store32(timeout_address, 0u); + return error::kWaitTimeout; + }); + }; + }; + hle.add("ThreadManForUser", "sceKernelSendMsgPipe", send(true)); + hle.add("ThreadManForUser", "sceKernelSendMsgPipeCB", send(true)); + hle.add("ThreadManForUser", "sceKernelTrySendMsgPipe", send(false)); + + const auto receive = [](bool may_wait) { + return [may_wait](Runtime &rt, AllegrexContext &ctx) { + const SceUID uid = as_signed(arg(ctx, 0)); + const auto found = msg_pipes().find(uid); + if (found == msg_pipes().end()) { + kernel().finish(ctx, kUnknownMppid); + return; + } + auto &memory = rt.memory(); + const std::uint32_t address = arg(ctx, 1); + const std::uint32_t size = arg(ctx, 2); + const bool whole = arg(ctx, 3) == 0u; + const std::uint32_t result_address = ctx.gpr[8]; // t0 + const std::uint32_t timeout_address = ctx.gpr[9]; // t1 + kernel_pipes::MessagePipe &pipe = found->second.pipe; + if (!may_wait) { + const auto got = pipe.try_receive(size, whole); + if (got) write_bytes(memory, address, *got); + if (result_address != 0u && got) memory.store32(result_address, static_cast(got->size())); + kernel().finish(ctx, got ? 0u : kMppEmpty); + return; + } + const auto ticket = pipe.receive(size, whole); + const std::uint64_t generation = pipe.cancel_generation(); + std::optional timeout; + if (timeout_address != 0u) timeout = memory.load32(timeout_address); + kernel().wait_host(ctx, timeout, [uid, ticket, generation, address, result_address, timeout_address, + &memory](bool timed_out) -> std::optional { + const auto pipe = msg_pipes().find(uid); + if (pipe == msg_pipes().end()) return error::kWaitDelete; + kernel_pipes::MessagePipe &waited = pipe->second.pipe; + if (waited.cancel_generation() != generation) return kWaitCancel; + if (const auto got = waited.received(ticket)) { + write_bytes(memory, address, *got); + if (result_address != 0u) memory.store32(result_address, static_cast(got->size())); + return 0u; + } + if (!timed_out) return std::nullopt; + (void)waited.withdraw(ticket); + if (result_address != 0u) memory.store32(result_address, 0u); + if (timeout_address != 0u) memory.store32(timeout_address, 0u); + return error::kWaitTimeout; + }); + }; + }; + hle.add("ThreadManForUser", "sceKernelReceiveMsgPipe", receive(true)); + hle.add("ThreadManForUser", "sceKernelReceiveMsgPipeCB", receive(true)); + hle.add("ThreadManForUser", "sceKernelTryReceiveMsgPipe", receive(false)); + + // sceKernelCancelMsgPipe(uid, int *senders, int *receivers): every waiter + // ends with WAIT_CANCEL, and the pipe is emptied. + hle.add("ThreadManForUser", "sceKernelCancelMsgPipe", [](Runtime &rt, AllegrexContext &ctx) { + const auto found = msg_pipes().find(as_signed(arg(ctx, 0))); + if (found == msg_pipes().end()) { + kernel().finish(ctx, kUnknownMppid); + return; + } + log_once("mpp-cancel", "[kernel] sceKernelCancelMsgPipe (UNVERIFIED: no game traced yet)"); + kernel_pipes::MessagePipe &pipe = found->second.pipe; + const std::uint32_t senders = pipe.waiting_senders(); + const std::uint32_t receivers = pipe.waiting_receivers(); + (void)pipe.cancel(); + pipe.clear(); + auto &memory = rt.memory(); + if (arg(ctx, 1) != 0u) memory.store32(arg(ctx, 1), senders); + if (arg(ctx, 2) != 0u) memory.store32(arg(ctx, 2), receivers); + kernel().finish(ctx, 0u); + }); + // SceKernelMppInfo: size, name[32], attr, bufSize, freeSize, + // numSendWaitThreads, numReceiveWaitThreads. + hle.add("ThreadManForUser", "sceKernelReferMsgPipeStatus", [](Runtime &rt, AllegrexContext &ctx) { + const auto found = msg_pipes().find(as_signed(arg(ctx, 0))); + if (found == msg_pipes().end()) { + kernel().finish(ctx, kUnknownMppid); + return; + } + log_once("mpp-refer", "[kernel] sceKernelReferMsgPipeStatus (UNVERIFIED: no game traced yet)"); + const MsgPipe &pipe = found->second; + auto &memory = rt.memory(); + const std::uint32_t info = arg(ctx, 1); + if (info != 0u && memory.load32(info) >= 56u) { + for (std::uint32_t i = 0; i < 32u; ++i) + memory.store8(info + 4u + i, i < pipe.name.size() ? static_cast(pipe.name[i]) : 0u); + memory.store32(info + 36u, pipe.attributes); + memory.store32(info + 40u, pipe.pipe.capacity()); + memory.store32(info + 44u, pipe.pipe.free_space()); + memory.store32(info + 48u, pipe.pipe.waiting_senders()); + memory.store32(info + 52u, pipe.pipe.waiting_receivers()); + } + kernel().finish(ctx, 0u); + }); +} } // namespace void register_threadman(HleRegistrar &hle) { @@ -1324,6 +1534,7 @@ void register_threadman(HleRegistrar &hle) { register_mailboxes(hle); register_vpls(hle); register_fpls(hle); + register_msg_pipes(hle); register_callbacks_and_timers(hle); register_kernel_library(hle); } diff --git a/host/kernel/message_pipe.cpp b/host/kernel/message_pipe.cpp new file mode 100644 index 0000000..c301abb --- /dev/null +++ b/host/kernel/message_pipe.cpp @@ -0,0 +1,160 @@ +#include "message_pipe.hpp" + +#include + +namespace portablekit::kernel_pipes { + +std::uint32_t MessagePipe::room() const { + if (capacity_ != 0u) return free_space(); + if (receivers_.empty()) return 0u; + const Receive &first = receives_.at(receivers_.front()); + const std::uint32_t wanted = first.size - static_cast(first.bytes.size()); + return wanted > available() ? wanted - available() : 0u; +} + +void MessagePipe::pump() { + for (bool moved = true; moved;) { + moved = false; + // The first receiver takes what is there. + if (!receivers_.empty()) { + Receive &first = receives_.at(receivers_.front()); + const std::uint32_t wanted = first.size - static_cast(first.bytes.size()); + const std::uint32_t take = std::min(wanted, available()); + if (take != 0u) { + first.bytes.insert(first.bytes.end(), buffer_.begin(), buffer_.begin() + take); + buffer_.erase(buffer_.begin(), buffer_.begin() + take); + moved = true; + } + if (first.bytes.size() == first.size || (!first.whole && !first.bytes.empty())) { + first.done = true; + receivers_.pop_front(); + moved = true; + } + } + // The first sender puts in what fits. + if (!senders_.empty()) { + Send &first = sends_.at(senders_.front()); + const std::uint32_t left = static_cast(first.bytes.size()) - first.moved; + const std::uint32_t put = std::min(left, room()); + if (put != 0u) { + buffer_.insert(buffer_.end(), first.bytes.begin() + first.moved, first.bytes.begin() + first.moved + put); + first.moved += put; + moved = true; + } + if (first.moved == first.bytes.size() || (!first.whole && first.moved != 0u)) { + first.done = true; + senders_.pop_front(); + moved = true; + } + } + } +} + +MessagePipe::Ticket MessagePipe::send(std::vector bytes, bool whole) { + const Ticket ticket = next_ticket_++; + sends_[ticket] = Send{std::move(bytes), 0u, whole, false}; + senders_.push_back(ticket); + pump(); + return ticket; +} + +MessagePipe::Ticket MessagePipe::receive(std::uint32_t size, bool whole) { + const Ticket ticket = next_ticket_++; + Receive request; + request.size = size; + request.whole = whole; + receives_[ticket] = std::move(request); + receivers_.push_back(ticket); + pump(); + return ticket; +} + +std::optional MessagePipe::sent(Ticket ticket) { + pump(); + const auto found = sends_.find(ticket); + if (found == sends_.end() || !found->second.done) return std::nullopt; + const std::uint32_t moved = found->second.moved; + sends_.erase(found); + return moved; +} + +std::optional> MessagePipe::received(Ticket ticket) { + pump(); + const auto found = receives_.find(ticket); + if (found == receives_.end() || !found->second.done) return std::nullopt; + std::vector bytes = std::move(found->second.bytes); + receives_.erase(found); + return bytes; +} + +std::uint32_t MessagePipe::withdraw(Ticket ticket) { + std::uint32_t count = 0u; + if (const auto send = sends_.find(ticket); send != sends_.end()) { + count = send->second.moved; + sends_.erase(send); + senders_.erase(std::remove(senders_.begin(), senders_.end(), ticket), senders_.end()); + } + if (const auto receive = receives_.find(ticket); receive != receives_.end()) { + // What it had taken goes back in front, so nothing is lost. + const std::vector &taken = receive->second.bytes; + count = static_cast(taken.size()); + if (!receive->second.done) buffer_.insert(buffer_.begin(), taken.begin(), taken.end()); + receives_.erase(receive); + receivers_.erase(std::remove(receivers_.begin(), receivers_.end(), ticket), receivers_.end()); + } + pump(); + return count; +} + +std::optional MessagePipe::try_send(const std::vector &bytes, bool whole) { + if (!senders_.empty()) return std::nullopt; + const auto size = static_cast(bytes.size()); + // With no buffer the bytes can only go to receivers already waiting. + std::uint32_t space = room(); + if (capacity_ == 0u) { + space = 0u; + for (const Ticket ticket : receivers_) { + const Receive &waiting = receives_.at(ticket); + space += waiting.size - static_cast(waiting.bytes.size()); + } + space = space > available() ? space - available() : 0u; + } + const std::uint32_t put = std::min(size, space); + if (put == 0u && size != 0u) return std::nullopt; + if (whole && put < size) return std::nullopt; + std::vector part(bytes.begin(), bytes.begin() + put); + const Ticket ticket = send(std::move(part), true); + const auto moved = sent(ticket); + return moved.value_or(withdraw(ticket)); +} + +std::optional> MessagePipe::try_receive(std::uint32_t size, bool whole) { + if (!receivers_.empty()) return std::nullopt; + pump(); + // What waiting senders still hold counts too: taking from the buffer + // makes room for it. + std::uint32_t there = available(); + for (const Ticket ticket : senders_) { + const Send &waiting = sends_.at(ticket); + there += static_cast(waiting.bytes.size()) - waiting.moved; + } + const std::uint32_t take = std::min(size, there); + if (take == 0u && size != 0u) return std::nullopt; + if (whole && take < size) return std::nullopt; + const Ticket ticket = receive(take, true); + if (auto bytes = received(ticket)) return bytes; + (void)withdraw(ticket); + return std::nullopt; +} + +std::uint32_t MessagePipe::cancel() { + const auto count = static_cast(senders_.size() + receivers_.size()); + for (const Ticket ticket : senders_) sends_.erase(ticket); + for (const Ticket ticket : receivers_) receives_.erase(ticket); + senders_.clear(); + receivers_.clear(); + ++generation_; + return count; +} + +} // namespace portablekit::kernel_pipes diff --git a/host/kernel/message_pipe.hpp b/host/kernel/message_pipe.hpp new file mode 100644 index 0000000..f737112 --- /dev/null +++ b/host/kernel/message_pipe.hpp @@ -0,0 +1,94 @@ +#pragma once + +// The bookkeeping of a message pipe (sceKernel*MsgPipe), separate from the +// HLE calls so it can be tested without a kernel: the bytes in the pipe's +// buffer, and the senders and receivers waiting on it, served first come, +// first served. +// +// A pipe has a buffer of `capacity` bytes (it may be 0). A send puts bytes in +// and a receive takes them out, in order. Each waits in one of two modes: +// whole (the call ends once all of its bytes have gone or come) or partial +// (it ends once any have). A request moves as many bytes as it can whenever +// the pipe changes, so a send larger than the buffer goes through in pieces +// while a receiver takes them. With no buffer, bytes go only to a receiver +// that is waiting for them. + +#include +#include +#include +#include +#include + +namespace portablekit::kernel_pipes { + +class MessagePipe { +public: + using Ticket = std::uint64_t; + + explicit MessagePipe(std::uint32_t capacity = 0u) : capacity_(capacity) {} + + [[nodiscard]] std::uint32_t capacity() const noexcept { return capacity_; } + [[nodiscard]] std::uint32_t available() const noexcept { return static_cast(buffer_.size()); } + [[nodiscard]] std::uint32_t free_space() const noexcept { return capacity_ - available(); } + [[nodiscard]] std::uint32_t waiting_senders() const noexcept { return static_cast(senders_.size()); } + [[nodiscard]] std::uint32_t waiting_receivers() const noexcept { + return static_cast(receivers_.size()); + } + + // Queues a send or a receive, moves what can be moved, and returns its + // ticket. Poll it with sent()/received() until it is done. + Ticket send(std::vector bytes, bool whole); + Ticket receive(std::uint32_t size, bool whole); + + // Done: how many bytes went. Not done: nothing. + [[nodiscard]] std::optional sent(Ticket ticket); + // Done: the bytes that came. Not done: nothing. + [[nodiscard]] std::optional> received(Ticket ticket); + + // A request that stops waiting (a timeout): it leaves the queue. Bytes a + // whole send had already put in stay there; bytes a whole receive had + // already taken go back to the front of the buffer. Returns that count. + std::uint32_t withdraw(Ticket ticket); + + // The Try forms: done at once or not at all, and never ahead of a thread + // already waiting. Empty when a PSP answers "full" or "empty". + [[nodiscard]] std::optional try_send(const std::vector &bytes, bool whole); + [[nodiscard]] std::optional> try_receive(std::uint32_t size, bool whole); + + // Every waiting request is dropped; waits end with the cancel error. + // Returns how many there were. The generation lets a waiter tell. + std::uint32_t cancel(); + // Empties the buffer. + void clear() { buffer_.clear(); } + [[nodiscard]] std::uint64_t cancel_generation() const noexcept { return generation_; } + +private: + struct Send { + std::vector bytes; + std::uint32_t moved{}; + bool whole{}; + bool done{}; + }; + struct Receive { + std::uint32_t size{}; + std::vector bytes; + bool whole{}; + bool done{}; + }; + + void pump(); + // What a sender may put in now: the free buffer, or with no buffer what + // the first waiting receiver still wants beyond what is already there. + [[nodiscard]] std::uint32_t room() const; + + std::uint32_t capacity_{}; + std::deque buffer_; + Ticket next_ticket_{1u}; + std::uint64_t generation_{}; + std::deque senders_; + std::deque receivers_; + std::map sends_; + std::map receives_; +}; + +} // namespace portablekit::kernel_pipes diff --git a/tests/pipe_tests.cpp b/tests/pipe_tests.cpp new file mode 100644 index 0000000..10d0598 --- /dev/null +++ b/tests/pipe_tests.cpp @@ -0,0 +1,91 @@ +// Message pipes (sceKernel*MsgPipe): the buffer, whole and partial sends and +// receives, the order waiting threads are served in, and pipes with no +// buffer. No kernel, no game. + +#include "kernel/message_pipe.hpp" + +#include +#include + +namespace { + +int g_failures = 0; + +void check(bool condition, const char *what) { + std::printf("%s %s\n", condition ? "ok " : "FAIL", what); + if (!condition) ++g_failures; +} + +std::vector bytes(std::uint32_t count, std::uint8_t first = 1u) { + std::vector result(count); + for (std::uint32_t i = 0; i < count; ++i) result[i] = static_cast(first + i); + return result; +} + +} // namespace + +int main() { + using portablekit::kernel_pipes::MessagePipe; + + { + MessagePipe pipe(0x400u); + check(pipe.try_send(bytes(0x54u), true) == 0x54u && pipe.available() == 0x54u, + "a message that fits goes into the buffer at once"); + const auto got = pipe.try_receive(8u, true); + check(got && got->size() == 8u && (*got)[0] == 1u && (*got)[7] == 8u, "a receive takes the first bytes"); + check(!pipe.try_receive(0x100u, true) && pipe.available() == 0x4Cu, + "a whole receive of more than there is fails and takes nothing"); + const auto rest = pipe.try_receive(0x100u, false); + check(rest && rest->size() == 0x4Cu && pipe.available() == 0u, "a partial receive takes what there is"); + check(!pipe.try_receive(4u, false), "an empty pipe has nothing for a partial receive either"); + } + { + MessagePipe pipe(16u); + check(!pipe.try_send(bytes(20u), true) && pipe.available() == 0u, + "a whole send larger than the free space fails and puts nothing in"); + check(pipe.try_send(bytes(20u), false) == 16u, "a partial send puts in what fits"); + const auto sender = pipe.send(bytes(8u, 100u), true); + check(!pipe.sent(sender) && pipe.waiting_senders() == 1u, "a send with no room waits"); + check(!pipe.try_send(bytes(1u), false), "a try does not go ahead of a waiting sender"); + const auto first = pipe.try_receive(16u, true); + check(first && (*first)[15] == 16u, "the receiver gets the bytes in the order they were sent"); + check(pipe.sent(sender) == 8u && pipe.available() == 8u, "the waiting send completes once there is room"); + } + { + MessagePipe pipe(4u); + const auto receiver = pipe.receive(10u, true); + const auto sender = pipe.send(bytes(10u), true); + const auto got = pipe.received(receiver); + check(got && got->size() == 10u && (*got)[9] == 10u, + "a whole send larger than the buffer goes through in pieces to a waiting receiver"); + check(pipe.sent(sender) == 10u, "and the send completes with every byte"); + } + { + MessagePipe pipe(0u); + check(!pipe.try_send(bytes(4u), true), "with no buffer and no receiver a send cannot go"); + const auto sender = pipe.send(bytes(4u), true); + check(!pipe.sent(sender), "so it waits"); + const auto got = pipe.try_receive(4u, true); + check(got && got->size() == 4u && pipe.sent(sender) == 4u, "a receiver takes it straight from the sender"); + const auto waiting = pipe.receive(3u, true); + check(pipe.try_send(bytes(3u), true) == 3u && pipe.received(waiting)->size() == 3u, + "a try send hands bytes to a waiting receiver"); + } + { + MessagePipe pipe(8u); + const auto a = pipe.receive(4u, true); + const auto b = pipe.receive(4u, true); + check(pipe.waiting_receivers() == 2u, "receivers wait in order"); + const auto sent = pipe.try_send(bytes(6u), true); + check(sent == 6u && pipe.received(a) && !pipe.received(b), "the first receiver is served first"); + check(pipe.withdraw(b) == 2u && pipe.waiting_receivers() == 0u && pipe.available() == 2u, + "a receiver that times out leaves the queue, and what it had taken goes back"); + const auto c = pipe.receive(4u, true); + const std::uint64_t generation = pipe.cancel_generation(); + check(pipe.cancel() == 1u && pipe.cancel_generation() != generation && !pipe.received(c), + "cancelling drops every waiter and says how many"); + } + + if (g_failures != 0) std::printf("%d failed\n", g_failures); + return g_failures == 0 ? 0 : 1; +} From e19c7bc21255e216ff352d87d8daa420a9332c5b Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:22:02 +0300 Subject: [PATCH 08/26] Implement scePsmf, the PSMF header library SetPsmf, VerifyPsmf, GetPsmfVersion, GetNumberOfStreams, GetNumberOfSpecificStreams, SpecifyStream (and WithStreamType, WithStreamTypeNumber), GetCurrentStreamNumber, GetCurrentStreamType, GetVideoInfo, GetAudioInfo, GetHeaderSize, GetStreamSize and the presentation start and end times. The header is parsed on the host (movie/psmf_header.*, with unit tests on a header built in the traced layout) and kept by the game's SceuPsmf address. Traced in Patapon (UCES00995), which checks its intro movie with them when a new game starts: its header gives 2 streams, AVC 480x272 and ATRAC3plus. The calls it makes are traced; the rest say UNVERIFIED on first use, and the error codes are the library's documented ones. NIDs match SHA-1 of the names. _TRACE_MPEG traces the calls. Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 10 ++ cmake/PortableKit.cmake | 2 + configs/nids.csv | 16 +++ host/hle/hle_common.hpp | 1 + host/hle/hle_psmf.cpp | 239 +++++++++++++++++++++++++++++++++++++ host/movie/psmf_header.cpp | 55 +++++++++ host/movie/psmf_header.hpp | 50 ++++++++ host/system.cpp | 1 + tests/psmf_tests.cpp | 69 +++++++++++ 9 files changed, 443 insertions(+) create mode 100644 host/hle/hle_psmf.cpp create mode 100644 host/movie/psmf_header.cpp create mode 100644 host/movie/psmf_header.hpp create mode 100644 tests/psmf_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ea55481..748546f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -244,6 +244,16 @@ if(PSPRECOMP_BUILD_TESTS) add_test(NAME portablekit_pipe_tests COMMAND portablekit_pipe_tests) endif() +# The PSMF header as scePsmf reads it. +if(PSPRECOMP_BUILD_TESTS) + add_executable(portablekit_psmf_tests + "${PORTABLEKIT_ROOT}/tests/psmf_tests.cpp" + "${PORTABLEKIT_ROOT}/host/movie/psmf_header.cpp") + target_include_directories(portablekit_psmf_tests PRIVATE "${PORTABLEKIT_ROOT}/host") + set_target_properties(portablekit_psmf_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + add_test(NAME portablekit_psmf_tests COMMAND portablekit_psmf_tests) +endif() + # Each platform's settings defaults. if(PSPRECOMP_BUILD_TESTS) add_executable(portablekit_settings_tests diff --git a/cmake/PortableKit.cmake b/cmake/PortableKit.cmake index 8e36e13..d106589 100644 --- a/cmake/PortableKit.cmake +++ b/cmake/PortableKit.cmake @@ -29,6 +29,7 @@ set(PORTABLEKIT_HOST_SOURCES host/audio/atrac_decoder.cpp host/movie/avc_decoder.cpp host/movie/psmf_demuxer.cpp + host/movie/psmf_header.cpp host/audio/audio_sink.cpp host/audio/sas_core.cpp host/camera_probe.cpp @@ -59,6 +60,7 @@ set(PORTABLEKIT_HOST_SOURCES host/hle/hle_atrac.cpp host/hle/hle_mpeg.cpp host/hle/hle_psmfplayer.cpp + host/hle/hle_psmf.cpp host/fonts/game_font.cpp host/hle/hle_font.cpp host/hle/hle_utility.cpp diff --git a/configs/nids.csv b/configs/nids.csv index 7dd5045..c0f11af 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -463,6 +463,22 @@ UtilsForUser,0x920F104A,sceKernelIcacheInvalidateAll UtilsForUser,0xB435DEC5,sceKernelDcacheWritebackInvalidateAll UtilsForUser,0xBFA98062,sceKernelDcacheInvalidateRange UtilsForUser,0xC2DF770E,sceKernelIcacheInvalidateRange +scePsmf,0x0BA514E5,scePsmfGetVideoInfo +scePsmf,0x0C120E1D,scePsmfSpecifyStreamWithStreamTypeNumber +scePsmf,0x1E6D9013,scePsmfSpecifyStreamWithStreamType +scePsmf,0x2673646B,scePsmfVerifyPsmf +scePsmf,0x28240568,scePsmfGetCurrentStreamNumber +scePsmf,0x4BC9BDE0,scePsmfSpecifyStream +scePsmf,0x68D42328,scePsmfGetNumberOfSpecificStreams +scePsmf,0x76D3AEBA,scePsmfGetPresentationStartTime +scePsmf,0xA5EBFE81,scePsmfGetStreamSize +scePsmf,0xA83F7113,scePsmfGetAudioInfo +scePsmf,0xB78EB9E9,scePsmfGetHeaderSize +scePsmf,0xBD8AE0D8,scePsmfGetPresentationEndTime +scePsmf,0xC22C8327,scePsmfSetPsmf +scePsmf,0xC7DB3A5B,scePsmfGetCurrentStreamType +scePsmf,0xE1283895,scePsmfGetPsmfVersion +scePsmf,0xEAED89CD,scePsmfGetNumberOfStreams scePsmfPlayer,0x1078C008,scePsmfPlayerStop scePsmfPlayer,0x1E57A8E7,scePsmfPlayerConfigPlayer scePsmfPlayer,0x235D8787,scePsmfPlayerCreate diff --git a/host/hle/hle_common.hpp b/host/hle/hle_common.hpp index 1495529..0ba1bb9 100644 --- a/host/hle/hle_common.hpp +++ b/host/hle/hle_common.hpp @@ -79,6 +79,7 @@ void register_media(HleRegistrar &hle); void register_atrac(HleRegistrar &hle); void register_mpeg(HleRegistrar &hle); void register_psmfplayer(HleRegistrar &hle); +void register_psmf(HleRegistrar &hle); void register_font(HleRegistrar &hle); void register_utility(HleRegistrar &hle, const std::filesystem::path &memory_stick); void register_savedata(HleRegistrar &hle, const std::filesystem::path &memory_stick); diff --git a/host/hle/hle_psmf.cpp b/host/hle/hle_psmf.cpp new file mode 100644 index 0000000..9e80c35 --- /dev/null +++ b/host/hle/hle_psmf.cpp @@ -0,0 +1,239 @@ +// scePsmf: the library a game uses to read a PSMF movie's header before it +// feeds the stream data to sceMpeg itself: how many streams of which kind +// there are, which one to play, and the picture size. The game hands over +// a SceuPsmf of its own; what the library keeps in it is not the game's to +// read, so the parsed header is kept here, by that structure's address. +// +// Traced in Patapon (UCES00995), which checks a new game's intro movie this +// way: SetPsmf, GetPsmfVersion (must not be negative), the number of +// streams of types 0 to 3 and in all, then for each stream SpecifyStream, +// GetCurrentStreamType and, for video, GetVideoInfo (width, height). The +// calls no traced game has made yet say UNVERIFIED on their first use, and +// the error codes are the library's as documented, not traced. + +#include "../profile.hpp" +#include "hle_common.hpp" + +#include "movie/psmf_header.hpp" +#include "psprecomp/common.hpp" + +#include +#include +#include +#include +#include + +namespace portablekit { +namespace { + +constexpr std::uint32_t kNotInitialized = 0x80615001u; // no SetPsmf on this structure +constexpr std::uint32_t kNotFound = 0x80615025u; // no such stream +constexpr std::uint32_t kInvalidPsmf = 0x80615501u; // not a PSMF header +constexpr std::uint32_t kInvalidId = 0x80615100u; // the current stream is of another kind + +// A header and its stream table fit in the first sector of the file. +constexpr std::uint32_t kHeaderBytes = 2048u; + +struct Psmf { + movie::PsmfHeader header; + int current{-1}; +}; + +std::map &psmfs() { + static std::map table; + return table; +} + +bool trace_psmf() { + static const bool enabled = portablekit::env("TRACE_MPEG") != nullptr; + return enabled; +} + +void unverified(const char *name) { + static std::set said; + if (said.insert(name).second) std::cerr << "[psmf] " << name << " (UNVERIFIED: no game traced yet)\n"; +} + +void finish(AllegrexContext &ctx, const char *name, std::uint32_t result) { + if (trace_psmf()) + std::cerr << "[psmf] " << name << "(" << psprecomp::hex32(arg(ctx, 0)) << ", " << psprecomp::hex32(arg(ctx, 1)) + << ", " << psprecomp::hex32(arg(ctx, 2)) << ") -> " << psprecomp::hex32(result) << "\n"; + kernel().finish(ctx, result); +} + +std::optional read_header(const psprecomp::GuestMemory &memory, std::uint32_t address) { + if (!memory.contains(address, kHeaderBytes)) return std::nullopt; + std::vector bytes(kHeaderBytes); + memory.copy_out(address, bytes); + return movie::parse_psmf_header(bytes); +} + +// The structure's state, or the error that ends the call. +Psmf *find(AllegrexContext &ctx, const char *name) { + const auto found = psmfs().find(arg(ctx, 0)); + if (found == psmfs().end()) { + finish(ctx, name, kNotInitialized); + return nullptr; + } + return &found->second; +} + +const movie::PsmfStream *current(Psmf &psmf) { + if (psmf.current < 0 || static_cast(psmf.current) >= psmf.header.streams.size()) return nullptr; + return &psmf.header.streams[static_cast(psmf.current)]; +} + +} // namespace + +void register_psmf(HleRegistrar &hle) { + // scePsmfSetPsmf(psmf, data): the header at `data` describes the movie. + hle.add("scePsmf", "scePsmfSetPsmf", [](Runtime &rt, AllegrexContext &ctx) { + const auto header = read_header(rt.memory(), arg(ctx, 1)); + if (!header) { + finish(ctx, "scePsmfSetPsmf", kInvalidPsmf); + return; + } + psmfs()[arg(ctx, 0)] = Psmf{*header, -1}; + finish(ctx, "scePsmfSetPsmf", 0u); + }); + hle.add("scePsmf", "scePsmfVerifyPsmf", [](Runtime &rt, AllegrexContext &ctx) { + unverified("scePsmfVerifyPsmf"); + finish(ctx, "scePsmfVerifyPsmf", read_header(rt.memory(), arg(ctx, 0)) ? 0u : kInvalidPsmf); + }); + // The version's four ASCII digits as a word, first byte lowest. The + // game traced only tests that it is not negative. + hle.add("scePsmf", "scePsmfGetPsmfVersion", [](Runtime &, AllegrexContext &ctx) { + if (Psmf *psmf = find(ctx, "scePsmfGetPsmfVersion")) finish(ctx, "scePsmfGetPsmfVersion", psmf->header.version); + }); + hle.add("scePsmf", "scePsmfGetNumberOfStreams", [](Runtime &, AllegrexContext &ctx) { + if (Psmf *psmf = find(ctx, "scePsmfGetNumberOfStreams")) + finish(ctx, "scePsmfGetNumberOfStreams", static_cast(psmf->header.streams.size())); + }); + // (psmf, type): 0 AVC, 1 ATRAC3plus, 2 PCM, 3 anything else. + hle.add("scePsmf", "scePsmfGetNumberOfSpecificStreams", [](Runtime &, AllegrexContext &ctx) { + Psmf *psmf = find(ctx, "scePsmfGetNumberOfSpecificStreams"); + if (psmf == nullptr) return; + std::uint32_t count = 0u; + for (const auto &stream : psmf->header.streams) + if (stream.type() == static_cast(arg(ctx, 1))) ++count; + finish(ctx, "scePsmfGetNumberOfSpecificStreams", count); + }); + // (psmf, index): the stream later calls are about. + hle.add("scePsmf", "scePsmfSpecifyStream", [](Runtime &, AllegrexContext &ctx) { + Psmf *psmf = find(ctx, "scePsmfSpecifyStream"); + if (psmf == nullptr) return; + const auto index = static_cast(arg(ctx, 1)); + if (index < 0 || static_cast(index) >= psmf->header.streams.size()) { + finish(ctx, "scePsmfSpecifyStream", kNotFound); + return; + } + psmf->current = index; + finish(ctx, "scePsmfSpecifyStream", 0u); + }); + // (psmf, type, channel) + hle.add("scePsmf", "scePsmfSpecifyStreamWithStreamType", [](Runtime &, AllegrexContext &ctx) { + Psmf *psmf = find(ctx, "scePsmfSpecifyStreamWithStreamType"); + if (psmf == nullptr) return; + unverified("scePsmfSpecifyStreamWithStreamType"); + for (std::size_t i = 0; i < psmf->header.streams.size(); ++i) { + const auto &stream = psmf->header.streams[i]; + if (stream.type() == static_cast(arg(ctx, 1)) && stream.channel() == static_cast(arg(ctx, 2))) { + psmf->current = static_cast(i); + finish(ctx, "scePsmfSpecifyStreamWithStreamType", 0u); + return; + } + } + finish(ctx, "scePsmfSpecifyStreamWithStreamType", kNotFound); + }); + // (psmf, type, n): the n-th stream of that type. + hle.add("scePsmf", "scePsmfSpecifyStreamWithStreamTypeNumber", [](Runtime &, AllegrexContext &ctx) { + Psmf *psmf = find(ctx, "scePsmfSpecifyStreamWithStreamTypeNumber"); + if (psmf == nullptr) return; + unverified("scePsmfSpecifyStreamWithStreamTypeNumber"); + std::uint32_t seen = 0u; + for (std::size_t i = 0; i < psmf->header.streams.size(); ++i) { + if (psmf->header.streams[i].type() != static_cast(arg(ctx, 1))) continue; + if (seen++ == arg(ctx, 2)) { + psmf->current = static_cast(i); + finish(ctx, "scePsmfSpecifyStreamWithStreamTypeNumber", 0u); + return; + } + } + finish(ctx, "scePsmfSpecifyStreamWithStreamTypeNumber", kNotFound); + }); + hle.add("scePsmf", "scePsmfGetCurrentStreamNumber", [](Runtime &, AllegrexContext &ctx) { + Psmf *psmf = find(ctx, "scePsmfGetCurrentStreamNumber"); + if (psmf == nullptr) return; + unverified("scePsmfGetCurrentStreamNumber"); + finish(ctx, "scePsmfGetCurrentStreamNumber", + psmf->current < 0 ? kNotFound : static_cast(psmf->current)); + }); + // (psmf, int *type, int *channel) + hle.add("scePsmf", "scePsmfGetCurrentStreamType", [](Runtime &rt, AllegrexContext &ctx) { + Psmf *psmf = find(ctx, "scePsmfGetCurrentStreamType"); + if (psmf == nullptr) return; + const movie::PsmfStream *stream = current(*psmf); + if (stream == nullptr) { + finish(ctx, "scePsmfGetCurrentStreamType", kNotFound); + return; + } + auto &memory = rt.memory(); + if (arg(ctx, 1) != 0u) memory.store32(arg(ctx, 1), static_cast(stream->type())); + if (arg(ctx, 2) != 0u) memory.store32(arg(ctx, 2), static_cast(stream->channel())); + finish(ctx, "scePsmfGetCurrentStreamType", 0u); + }); + // (psmf, info): width and height in pixels. + hle.add("scePsmf", "scePsmfGetVideoInfo", [](Runtime &rt, AllegrexContext &ctx) { + Psmf *psmf = find(ctx, "scePsmfGetVideoInfo"); + if (psmf == nullptr) return; + const movie::PsmfStream *stream = current(*psmf); + if (stream == nullptr || stream->type() != 0) { + finish(ctx, "scePsmfGetVideoInfo", stream == nullptr ? kNotFound : kInvalidId); + return; + } + if (arg(ctx, 1) != 0u) { + rt.memory().store32(arg(ctx, 1), stream->info[0] * 16u); + rt.memory().store32(arg(ctx, 1) + 4u, stream->info[1] * 16u); + } + finish(ctx, "scePsmfGetVideoInfo", 0u); + }); + // (psmf, info): channels, and the sample rate (code 2 is 44.1 kHz, the + // only rate PSMF audio has been seen at). + hle.add("scePsmf", "scePsmfGetAudioInfo", [](Runtime &rt, AllegrexContext &ctx) { + Psmf *psmf = find(ctx, "scePsmfGetAudioInfo"); + if (psmf == nullptr) return; + unverified("scePsmfGetAudioInfo"); + const movie::PsmfStream *stream = current(*psmf); + if (stream == nullptr || (stream->type() != 1 && stream->type() != 2)) { + finish(ctx, "scePsmfGetAudioInfo", stream == nullptr ? kNotFound : kInvalidId); + return; + } + if (arg(ctx, 1) != 0u) { + rt.memory().store32(arg(ctx, 1), stream->info[0]); + rt.memory().store32(arg(ctx, 1) + 4u, stream->info[1] == 2u ? 44100u : 0u); + } + finish(ctx, "scePsmfGetAudioInfo", 0u); + }); + // (psmf, unsigned *out) for each. + const auto write_out = [](const char *name, std::uint32_t (*value)(const movie::PsmfHeader &)) { + return [name, value](Runtime &rt, AllegrexContext &ctx) { + Psmf *psmf = find(ctx, name); + if (psmf == nullptr) return; + unverified(name); + if (arg(ctx, 1) != 0u) rt.memory().store32(arg(ctx, 1), value(psmf->header)); + finish(ctx, name, 0u); + }; + }; + hle.add("scePsmf", "scePsmfGetHeaderSize", + write_out("scePsmfGetHeaderSize", [](const movie::PsmfHeader &h) { return h.header_size; })); + hle.add("scePsmf", "scePsmfGetStreamSize", + write_out("scePsmfGetStreamSize", [](const movie::PsmfHeader &h) { return h.stream_size; })); + hle.add("scePsmf", "scePsmfGetPresentationStartTime", + write_out("scePsmfGetPresentationStartTime", + [](const movie::PsmfHeader &h) { return static_cast(h.start_time); })); + hle.add("scePsmf", "scePsmfGetPresentationEndTime", + write_out("scePsmfGetPresentationEndTime", + [](const movie::PsmfHeader &h) { return static_cast(h.end_time); })); +} + +} // namespace portablekit diff --git a/host/movie/psmf_header.cpp b/host/movie/psmf_header.cpp new file mode 100644 index 0000000..e026bd6 --- /dev/null +++ b/host/movie/psmf_header.cpp @@ -0,0 +1,55 @@ +#include "psmf_header.hpp" + +namespace portablekit::movie { +namespace { + +std::uint32_t be32(std::span bytes, std::size_t at) { + return static_cast(bytes[at]) << 24u | static_cast(bytes[at + 1u]) << 16u | + static_cast(bytes[at + 2u]) << 8u | bytes[at + 3u]; +} + +std::uint64_t be48(std::span bytes, std::size_t at) { + std::uint64_t value = 0u; + for (std::size_t i = 0; i < 6u; ++i) value = value << 8u | bytes[at + i]; + return value; +} + +} // namespace + +int PsmfStream::type() const noexcept { + if ((stream_id & 0xF0u) == 0xE0u) return 0; + if (stream_id == 0xBDu) { + if ((private_id & 0xF0u) == 0x00u) return 1; + if ((private_id & 0xF0u) == 0x10u) return 2; + } + return 3; +} + +int PsmfStream::channel() const noexcept { return (stream_id == 0xBDu ? private_id : stream_id) & 0x0Fu; } + +std::optional parse_psmf_header(std::span bytes) { + if (bytes.size() < kPsmfStreamTableOffset + 2u) return std::nullopt; + if (bytes[0] != 'P' || bytes[1] != 'S' || bytes[2] != 'M' || bytes[3] != 'F') return std::nullopt; + PsmfHeader header; + header.version = static_cast(bytes[4]) | static_cast(bytes[5]) << 8u | + static_cast(bytes[6]) << 16u | static_cast(bytes[7]) << 24u; + header.header_size = be32(bytes, 0x08u); + header.stream_size = be32(bytes, 0x0Cu); + header.start_time = be48(bytes, 0x54u); + header.end_time = be48(bytes, 0x5Au); + const std::size_t count = static_cast(bytes[0x80u]) << 8u | bytes[0x81u]; + const std::size_t table = kPsmfStreamTableOffset + 2u; + if (bytes.size() < table + count * kPsmfStreamEntrySize) return std::nullopt; + for (std::size_t i = 0; i < count; ++i) { + const std::size_t at = table + i * kPsmfStreamEntrySize; + PsmfStream stream; + stream.stream_id = bytes[at]; + stream.private_id = bytes[at + 1u]; + stream.info[0] = bytes[at + 12u]; + stream.info[1] = bytes[at + 13u]; + header.streams.push_back(stream); + } + return header; +} + +} // namespace portablekit::movie diff --git a/host/movie/psmf_header.hpp b/host/movie/psmf_header.hpp new file mode 100644 index 0000000..bb9531e --- /dev/null +++ b/host/movie/psmf_header.hpp @@ -0,0 +1,50 @@ +#pragma once + +// The header of a PSMF movie, as the scePsmf library reads it: the version, +// where the stream data starts and how long it is, the presentation times, +// and one 16-byte entry per elementary stream. Layout, all big-endian, as +// read off Patapon's (UCES00995) intro movie: +// +// 0x00 "PSMF" 0x04 version, four ASCII digits ("0014") +// 0x08 header size 0x0C stream data size +// 0x54 start time (48-bit, 90 kHz) 0x5A end time (48-bit) +// 0x80 number of streams (16-bit), then from 0x82 one entry each: +// +0 stream id (0xE0.. video, 0xBD private: audio) +// +1 private stream id (0x00.. ATRAC3plus, 0x10.. PCM) +// +12, +13 video: width and height in 16-pixel units +// audio: channels, and a sample-rate code + +#include +#include +#include +#include + +namespace portablekit::movie { + +inline constexpr std::size_t kPsmfStreamTableOffset = 0x80u; +inline constexpr std::size_t kPsmfStreamEntrySize = 16u; + +struct PsmfStream { + std::uint8_t stream_id{}; + std::uint8_t private_id{}; + std::uint8_t info[2]{}; // bytes 12 and 13 + // The types scePsmf reports: 0 AVC video, 1 ATRAC3plus, 2 PCM, 3 other. + [[nodiscard]] int type() const noexcept; + // The channel the type's numbering uses: the low nibble of the id. + [[nodiscard]] int channel() const noexcept; +}; + +struct PsmfHeader { + std::uint32_t version{}; // the four ASCII digits, first byte lowest + std::uint32_t header_size{}; + std::uint32_t stream_size{}; + std::uint64_t start_time{}; + std::uint64_t end_time{}; + std::vector streams; +}; + +// Empty if `bytes` does not start with a PSMF header or is too short for the +// stream table it announces. +[[nodiscard]] std::optional parse_psmf_header(std::span bytes); + +} // namespace portablekit::movie diff --git a/host/system.cpp b/host/system.cpp index 03fd09c..b95facc 100644 --- a/host/system.cpp +++ b/host/system.cpp @@ -89,6 +89,7 @@ void install_system(Runtime &runtime, const psprecomp::Elf32Image &elf, const Pr register_atrac(hle); register_mpeg(hle); register_psmfplayer(hle); + register_psmf(hle); register_font(hle); register_utility(hle, paths.memory_stick); register_adhoc(hle); diff --git a/tests/psmf_tests.cpp b/tests/psmf_tests.cpp new file mode 100644 index 0000000..87bed5c --- /dev/null +++ b/tests/psmf_tests.cpp @@ -0,0 +1,69 @@ +// The PSMF header as scePsmf reads it, from a header built here in the layout +// traced from a game's movie. No game data. + +#include "movie/psmf_header.hpp" + +#include +#include + +namespace { + +int g_failures = 0; + +void check(bool condition, const char *what) { + std::printf("%s %s\n", condition ? "ok " : "FAIL", what); + if (!condition) ++g_failures; +} + +void put_be(std::vector &bytes, std::size_t at, std::uint64_t value, std::size_t width) { + for (std::size_t i = 0; i < width; ++i) bytes[at + i] = static_cast(value >> (8u * (width - 1u - i))); +} + +std::vector header() { + std::vector bytes(2048u, 0u); + const char magic[] = "PSMF0014"; + for (std::size_t i = 0; i < 8u; ++i) bytes[i] = static_cast(magic[i]); + put_be(bytes, 0x08u, 0x800u, 4u); + put_be(bytes, 0x0Cu, 0x870800u, 4u); + put_be(bytes, 0x54u, 90000u, 6u); + put_be(bytes, 0x5Au, 0x542C85u, 6u); + put_be(bytes, 0x80u, 3u, 2u); + // Video 0xE0, 480x272; ATRAC3plus on private stream 0; PCM on 0x11. + bytes[0x82u] = 0xE0u; + bytes[0x82u + 12u] = 30u; + bytes[0x82u + 13u] = 17u; + bytes[0x92u] = 0xBDu; + bytes[0x92u + 12u] = 2u; + bytes[0x92u + 13u] = 2u; + bytes[0xA2u] = 0xBDu; + bytes[0xA3u] = 0x11u; + return bytes; +} + +} // namespace + +int main() { + using namespace portablekit::movie; + const auto bytes = header(); + const auto parsed = parse_psmf_header(bytes); + check(parsed.has_value(), "a PSMF header parses"); + if (parsed) { + check(parsed->header_size == 0x800u && parsed->stream_size == 0x870800u, "header and stream sizes"); + check(parsed->start_time == 90000u && parsed->end_time == 0x542C85u, "presentation times, 48-bit"); + check(parsed->version == 0x34313030u, "the version's digits, first byte lowest"); + check(parsed->streams.size() == 3u, "three streams"); + check(parsed->streams[0].type() == 0 && parsed->streams[0].info[0] * 16 == 480 && + parsed->streams[0].info[1] * 16 == 272, + "the video stream and its size"); + check(parsed->streams[1].type() == 1 && parsed->streams[1].channel() == 0 && parsed->streams[1].info[0] == 2u, + "ATRAC3plus on private stream 0, two channels"); + check(parsed->streams[2].type() == 2 && parsed->streams[2].channel() == 1, "PCM on private stream 0x11"); + } + std::vector wrong = bytes; + wrong[0] = 'X'; + check(!parse_psmf_header(wrong), "no magic, no header"); + std::vector short_table(bytes.begin(), bytes.begin() + 0xA0); + check(!parse_psmf_header(short_table), "a stream table cut short is refused"); + if (g_failures != 0) std::printf("%d failed\n", g_failures); + return g_failures == 0 ? 0 : 1; +} From 14d14eb2f00d5de4c9a45f8e93fd16a698419938 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:29:58 +0300 Subject: [PATCH 09/26] Implement sceMpegAvcCsc Converts a picture from a sceMpegAvcDecodeYCbCr buffer into pixels in the format sceMpegAvcDecodeMode set, frameWidth pixels a row. Traced in Patapon (UCES00995): its intro movie decodes to YCbCr in four buffers and converts each with range {0, 0, 480, 272}, in pixels, into two alternating framebuffers; with the stub the subtitles showed over the title screen instead of the movie. A range that is not the whole picture is logged as UNVERIFIED and the whole picture converted. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 1 + host/hle/hle_mpeg.cpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/configs/nids.csv b/configs/nids.csv index c0f11af..afa88e5 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -188,6 +188,7 @@ sceMpeg,0xF2930C9C,sceMpegAvcDecodeStopYCbCr sceMpeg,0xF5E7EA31,sceMpegAvcConvertToYuv420 sceMpeg,0xF8DCB679,sceMpegQueryAtracEsSize sceMpeg,0xFE246728,sceMpegGetAvcAu +sceMpeg,0x31BD0272,sceMpegAvcCsc sceNet,0x0BF0A3AE,sceNetGetLocalEtherAddr sceNet,0x281928A9,sceNetTerm sceNet,0x39AF39A6,sceNetInit diff --git a/host/hle/hle_mpeg.cpp b/host/hle/hle_mpeg.cpp index 0072f27..1c0aa48 100644 --- a/host/hle/hle_mpeg.cpp +++ b/host/hle/hle_mpeg.cpp @@ -573,6 +573,35 @@ void register_decoding(HleRegistrar &hle) { if (arg(ctx, 3) != 0u) memory.store32(arg(ctx, 3), produced ? 1u : 0u); finish_traced(ctx, "sceMpegAvcDecodeYCbCr", 0u, produced ? "picture " + std::to_string(state->pictures) : "none"); }); + // sceMpegAvcCsc(mpeg, ycbcr, range, frameWidth, dest): a picture from + // sceMpegAvcDecodeYCbCr's buffer into pixels in the format + // sceMpegAvcDecodeMode set, `frameWidth` pixels a row. `range` is + // {x, y, width, height}; Patapon (UCES00995) converts its intro movie + // with it, the whole picture each frame. + hle.add("sceMpeg", "sceMpegAvcCsc", [](Runtime &rt, AllegrexContext &ctx) { + MpegState *state = find_mpeg(arg(ctx, 0)); + auto &memory = rt.memory(); + const std::uint32_t range = arg(ctx, 2); + const std::uint32_t stride = arg(ctx, 3); + const std::uint32_t dest = ctx.gpr[8]; // t0 + const auto found = module().ycbcr.find(arg(ctx, 1)); + if (state == nullptr || found == module().ycbcr.end() || range == 0u || stride == 0u) { + finish_traced(ctx, "sceMpegAvcCsc", state == nullptr ? mpeg_error::kInvalidValue : 0u, "no picture", 5u); + return; + } + const std::uint32_t x = memory.load32(range), y = memory.load32(range + 4u); + const std::uint32_t width = memory.load32(range + 8u), height = memory.load32(range + 12u); + movie::Picture picture = found->second; + if (x != 0u || y != 0u || width < picture.width || height < picture.height) + log_once("mpeg-csc-range", "[mpeg] sceMpegAvcCsc converts part of a picture: x=" + std::to_string(x) + + " y=" + std::to_string(y) + " " + std::to_string(width) + "x" + + std::to_string(height) + " (UNVERIFIED: converting all of it)"); + store_pixels(memory, dest, stride, state->pixel_mode, picture); + finish_traced(ctx, "sceMpegAvcCsc", 0u, + std::to_string(x) + "," + std::to_string(y) + " " + std::to_string(width) + "x" + + std::to_string(height), + 5u); + }); // sceMpegAvcDecodeStopYCbCr(mpeg, bufferPointer, outStatus): pictures // the decoder still holds at the end of the stream. hle.add("sceMpeg", "sceMpegAvcDecodeStopYCbCr", [](Runtime &rt, AllegrexContext &ctx) { From 9df93c82386f0bb073d1ba3257f4c329d4a13abd Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:48:34 +0300 Subject: [PATCH 10/26] Let the clock move for a thread that reads it and never waits Emulated time moves when every thread waits, or after many dispatches without an import call. A thread that loops drawing and reading sceKernelGetSystemTime* without ever waiting satisfies neither, so it saw the same time for good: God of War (UCES00842) draws its logos in such a loop (seven GetSystemTimeWide calls, a GE list and a flip each time round) and stood at 1.2 s of emulated time after 90 s. Each clock read now charges the real time the calling thread has run since it last waited or was charged, once that is at least 1 ms and at most 50 ms at a time; timers and vblanks fire as the clock passes them. _NO_BUSY_CLOCK turns it off. Co-Authored-By: Claude Opus 5.5 (1M context) --- host/kernel/kernel.cpp | 20 ++++++++++++++++++++ host/kernel/kernel.hpp | 11 +++++++++++ 2 files changed, 31 insertions(+) diff --git a/host/kernel/kernel.cpp b/host/kernel/kernel.cpp index 4a90756..8753767 100644 --- a/host/kernel/kernel.cpp +++ b/host/kernel/kernel.cpp @@ -352,6 +352,7 @@ void Kernel::block(AllegrexContext &ctx, const WaitState &wait, std::uint32_t re ctx.set_gpr(2, error::kCanNotWait); return; } + ++waits_begun_; thread->wait = wait; if (wait.timeout_address != 0u) thread->wait.deadline_us = now_us_ + runtime_->memory().load32(wait.timeout_address); @@ -633,6 +634,25 @@ std::string Kernel::describe_threads() const { return report; } +void Kernel::charge_busy_time() { + static const bool off = portablekit::env("NO_BUSY_CLOCK") != nullptr; + if (off || interrupt_active_ || current_thread() == nullptr) return; + constexpr std::int64_t kBusyChargeMinUs = 1000; + constexpr std::int64_t kBusyChargeMaxUs = 50000; + const auto now = std::chrono::steady_clock::now(); + if (busy_thread_ != current_uid_ || busy_waits_ != waits_begun_) { + busy_thread_ = current_uid_; + busy_waits_ = waits_begun_; + busy_since_ = now; + return; + } + const std::int64_t ran_us = std::chrono::duration_cast(now - busy_since_).count(); + if (ran_us < kBusyChargeMinUs) return; + busy_since_ = now; + advance_clock(now_us_ + static_cast(std::min(ran_us, kBusyChargeMaxUs))); + process_timers(); +} + void Kernel::on_starvation(AllegrexContext &ctx) { if (interrupt_active_ || current_thread() == nullptr) return; advance_clock(now_us_ + kStarvationQuantumUs); diff --git a/host/kernel/kernel.hpp b/host/kernel/kernel.hpp index 019a08b..24158d1 100644 --- a/host/kernel/kernel.hpp +++ b/host/kernel/kernel.hpp @@ -346,6 +346,13 @@ class Kernel { // Periodic hook from the runtime's starvation boundary. void on_starvation(AllegrexContext &ctx); + // A thread that reads the clock again and again without ever waiting is + // using CPU time the clock should show: a PSP's loop that draws, reads + // the time and draws again sees it move. Called by the clock reads, this + // moves emulated time on by the real time the thread has run since it + // last waited or was charged (at most kBusyChargeMaxUs at a time), once + // that is at least kBusyChargeMinUs. _NO_BUSY_CLOCK turns it off. + void charge_busy_time(); // Guest entry stubs. void thread_exit_stub(AllegrexContext &ctx); @@ -386,6 +393,10 @@ class Kernel { std::uint64_t next_ready_sequence_{1u}; std::map> threads_; std::uint64_t now_us_{}; + std::uint64_t waits_begun_{}; // waits begun, so a busy thread can tell it has not waited + SceUID busy_thread_{}; + std::uint64_t busy_waits_{}; + std::chrono::steady_clock::time_point busy_since_{}; bool pacing_started_{}; bool pacing_fast_{}; // the hold lets emulated time run ahead: a load runs fast std::chrono::steady_clock::time_point pacing_real_base_{}; From 00857f97309edcec577070c2703ccbbb36c88450 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:48:35 +0300 Subject: [PATCH 11/26] Implement sceKernelReferThreadStatus, and charge busy clock reads SceKernelThreadInfo as pspthreadman.h lays it out, written up to the size the game gives: status (running 1, ready 2, waiting 4, suspended 8, dormant 16, dead 32), priorities, stack, wait type and object, exit status. God of War (UCES00842) polls it with DelayThread until its loader thread is done; with the stub it waited on the logo for good. The clock reads call Kernel::charge_busy_time. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 1 + host/hle/hle_threadman.cpp | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/configs/nids.csv b/configs/nids.csv index afa88e5..90d943c 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -454,6 +454,7 @@ ThreadManForUser,0xFBFA697D,sceKernelReceiveMsgPipeCB ThreadManForUser,0xDF52098F,sceKernelTryReceiveMsgPipe ThreadManForUser,0x349B864D,sceKernelCancelMsgPipe ThreadManForUser,0x33BE4024,sceKernelReferMsgPipeStatus +ThreadManForUser,0x17C1684E,sceKernelReferThreadStatus UtilsForUser,0x27CC57F0,sceKernelLibcTime UtilsForUser,0x3EE30821,sceKernelDcacheWritebackRange UtilsForUser,0x6AD345D7,sceKernelSetGPO diff --git a/host/hle/hle_threadman.cpp b/host/hle/hle_threadman.cpp index 987c94e..abfe547 100644 --- a/host/hle/hle_threadman.cpp +++ b/host/hle/hle_threadman.cpp @@ -8,6 +8,7 @@ #include "psprecomp/common.hpp" #include +#include #include #include #include @@ -185,6 +186,68 @@ void register_threads(HleRegistrar &hle) { if (trace_sync()) log_sync("ResumeThread " + std::to_string(thread->uid) + " " + thread->name); kernel().finish(ctx, 0u); // which runs it now if it outranks this thread }); + // sceKernelReferThreadStatus(uid, SceKernelThreadInfo *info), uid 0 the + // calling thread. The info's layout is pspthreadman.h's: size, name[32], + // attr, status, entry, stack, stackSize, gpReg, initPriority, + // currentPriority, waitType, waitId, wakeupCount, exitStatus, runClocks + // (8 bytes), three counters; written up to the size the game gives. + // God of War (UCES00842) polls it with DelayThread until a loader + // thread is done. + hle.add("ThreadManForUser", "sceKernelReferThreadStatus", [](Runtime &rt, AllegrexContext &ctx) { + const SceUID uid = as_signed(arg(ctx, 0)); + const Thread *thread = kernel().find_thread(uid == 0 ? kernel().current_uid() : uid); + if (thread == nullptr) { + kernel().finish(ctx, error::kUnknownThid); + return; + } + auto &memory = rt.memory(); + const std::uint32_t info = arg(ctx, 1); + const std::uint32_t size = info != 0u ? memory.load32(info) : 0u; + std::uint32_t status = 0u; + switch (thread->status) { + case ThreadStatus::Running: status = 1u; break; + case ThreadStatus::Ready: status = 2u; break; + case ThreadStatus::Waiting: status = 4u; break; + case ThreadStatus::Dormant: status = 16u; break; + case ThreadStatus::Dead: status = 32u; break; + } + if (thread->uid == kernel().current_uid()) status = 1u; + if (thread->suspended) status |= 8u; + std::uint32_t wait_type = 0u; + if (thread->status == ThreadStatus::Waiting) { + switch (thread->wait.type) { + case WaitType::Sleep: wait_type = 1u; break; + case WaitType::Delay: wait_type = 2u; break; + case WaitType::Semaphore: wait_type = 3u; break; + case WaitType::EventFlag: wait_type = 4u; break; + case WaitType::Mailbox: wait_type = 5u; break; + case WaitType::ThreadEnd: wait_type = 9u; break; + case WaitType::Mutex: wait_type = 12u; break; + default: wait_type = 0u; break; + } + } + std::array bytes{}; + const auto put = [&bytes](std::size_t at, std::uint32_t value) { + for (std::size_t i = 0; i < 4u; ++i) bytes[at + i] = static_cast(value >> (8u * i)); + }; + put(0u, size); + for (std::size_t i = 0; i < 31u && i < thread->name.size(); ++i) bytes[4u + i] = static_cast(thread->name[i]); + put(36u, thread->attributes); + put(40u, status); + put(44u, thread->entry); + put(48u, thread->stack_bottom); + put(52u, thread->stack_size); + put(56u, thread->gp); + put(60u, thread->initial_priority); + put(64u, thread->priority); + put(68u, wait_type); + put(72u, thread->status == ThreadStatus::Waiting ? as_unsigned(thread->wait.object) : 0u); + put(76u, thread->wakeup_count); + put(80u, as_unsigned(thread->exit_status)); + for (std::uint32_t i = 0; i < std::min(size, static_cast(bytes.size())); ++i) + memory.store8(info + i, bytes[i]); + kernel().finish(ctx, 0u); + }); // Runs the thread's notified callbacks now: 1 when there were any. hle.add("ThreadManForUser", "sceKernelCheckCallback", [](Runtime &, AllegrexContext &ctx) { kernel().finish(ctx, kernel().deliver_callbacks() ? 1u : 0u); @@ -206,14 +269,21 @@ void register_threads(HleRegistrar &hle) { } void register_time(HleRegistrar &hle) { + // Each clock read charges the time a thread that never waits has run + // (Kernel::charge_busy_time): God of War (UCES00842) draws its first + // screens in a loop that reads sceKernelGetSystemTimeWide and never + // waits, and without it the clock stood still at 1.2 s for good. hle.add("ThreadManForUser", "sceKernelGetSystemTime", [](Runtime &rt, AllegrexContext &ctx) { + kernel().charge_busy_time(); store64(rt.memory(), arg(ctx, 0), kernel().now_us()); kernel().finish(ctx, 0u); }); hle.add("ThreadManForUser", "sceKernelGetSystemTimeWide", [](Runtime &, AllegrexContext &ctx) { + kernel().charge_busy_time(); kernel().finish64(ctx, kernel().now_us()); }); hle.add("ThreadManForUser", "sceKernelGetSystemTimeLow", [](Runtime &, AllegrexContext &ctx) { + kernel().charge_busy_time(); kernel().finish(ctx, static_cast(kernel().now_us())); }); // (SceKernelSysClock *clock, seconds *, microseconds *): the clock in From a8751efeb356160beb97fcf97a4b8bcf8fe450b7 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:52:43 +0300 Subject: [PATCH 12/26] Answer sceAtracGetSecondBufferInfo: no second buffer is needed A looping streamed track loops from the bytes the game added the first time, so it never needs the file's end kept in a second buffer; the call answers 0x80630022 (second buffer not needed) with zero position and size. God of War (UCES00842) asks right after sceAtracSetDataAndGetID for its streamed music. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 1 + host/hle/hle_atrac.cpp | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/configs/nids.csv b/configs/nids.csv index 90d943c..6676347 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -71,6 +71,7 @@ sceAtrac3plus,0x0E2A73AB,sceAtracSetData sceAtrac3plus,0x0FAE370E,sceAtracSetHalfwayBufferAndGetID sceAtrac3plus,0x36FAABFB,sceAtracGetNextSample sceAtrac3plus,0x7DB31251,sceAtracAddStreamData +sceAtrac3plus,0x83E85EA0,sceAtracGetSecondBufferInfo sceAudio,0x01562BA3,sceAudioOutput2Reserve sceAudio,0x136CAF51,sceAudioOutputBlocking sceAudio,0x13F592BC,sceAudioOutputPannedBlocking diff --git a/host/hle/hle_atrac.cpp b/host/hle/hle_atrac.cpp index 911900a..0a75a07 100644 --- a/host/hle/hle_atrac.cpp +++ b/host/hle/hle_atrac.cpp @@ -42,6 +42,7 @@ inline constexpr std::uint32_t kNoData = 0x80630010u; inline constexpr std::uint32_t kSizeTooSmall = 0x80630011u; inline constexpr std::uint32_t kBadSample = 0x80630015u; inline constexpr std::uint32_t kNoLoopInformation = 0x80630021u; +inline constexpr std::uint32_t kSecondBufferNotNeeded = 0x80630022u; inline constexpr std::uint32_t kAllDataDecoded = 0x80630024u; } // namespace atrac_error @@ -760,6 +761,21 @@ void register_atrac_functions(HleRegistrar &hle) { << " from=" << write.file_offset; finish_traced(ctx, "sceAtracGetStreamDataInfo", 0u, details.str()); }); + // sceAtracGetSecondBufferInfo(id, outPosition, outBytes): where a + // looping streamed track wants the file's end kept, when its buffer + // cannot hold it. Tracks loop here from the bytes the game added the + // first time (see trim_stored), so none ever needs one. God of War + // (UCES00842) asks after setting up its streamed music. + hle.add("sceAtrac3plus", "sceAtracGetSecondBufferInfo", [](Runtime &rt, AllegrexContext &ctx) { + const std::uint32_t id = arg(ctx, 0); + if (find_context(id) == nullptr) { + finish_traced(ctx, "sceAtracGetSecondBufferInfo", context_error(id)); + return; + } + write_s32(rt.memory(), arg(ctx, 1), 0); + write_s32(rt.memory(), arg(ctx, 2), 0); + finish_traced(ctx, "sceAtracGetSecondBufferInfo", atrac_error::kSecondBufferNotNeeded); + }); // sceAtracAddStreamData(id, bytesAdded): the game has written that many // bytes where GetStreamDataInfo said. hle.add("sceAtrac3plus", "sceAtracAddStreamData", [](Runtime &rt, AllegrexContext &ctx) { From c7c73c9bce22c3875b09bde60f15d9fb92be24a2 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:26:26 +0300 Subject: [PATCH 13/26] Pass the corpus link, and any long compile, through a response file The app linked a game's library with every object on the command line. For PSP2i (NPJH50332, 385 units) that line passed the 32767 characters Windows starts a process with, and the link failed with error 206 after the whole recompile and compile had succeeded. The link now always goes through data/cache///link.rsp, and a compile command longer than 8000 characters through .rsp. The file quotes each argument and writes backslashes as forward slashes, so the GNU and the Windows rules for reading it agree. Tested: a unit test with 400 objects under a long cache path, and on Windows 11 a real llvm-mingw link of 400 objects through such a file (57 774 characters of arguments), which clang hands on to lld without trouble. A failed link is resumed by running compile again: the generated C++ and every object newer than its source are kept, so only the entry unit is compiled again before the link. Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 10 +++ apps/portablekit/CMakeLists.txt | 1 + apps/portablekit/src/corpus.cpp | 24 +++++-- apps/portablekit/src/response_file.cpp | 42 ++++++++++++ apps/portablekit/src/response_file.hpp | 32 +++++++++ tests/response_file_tests.cpp | 91 ++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 apps/portablekit/src/response_file.cpp create mode 100644 apps/portablekit/src/response_file.hpp create mode 100644 tests/response_file_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 748546f..c68a807 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -254,6 +254,16 @@ if(PSPRECOMP_BUILD_TESTS) add_test(NAME portablekit_psmf_tests COMMAND portablekit_psmf_tests) endif() +# The desktop app's response files for long compile and link command lines. +if(PSPRECOMP_BUILD_TESTS) + add_executable(portablekit_response_file_tests + "${PORTABLEKIT_ROOT}/tests/response_file_tests.cpp" + "${PORTABLEKIT_ROOT}/apps/portablekit/src/response_file.cpp") + target_include_directories(portablekit_response_file_tests PRIVATE "${PORTABLEKIT_ROOT}/apps/portablekit/src") + set_target_properties(portablekit_response_file_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + add_test(NAME portablekit_response_file_tests COMMAND portablekit_response_file_tests) +endif() + # Each platform's settings defaults. if(PSPRECOMP_BUILD_TESTS) add_executable(portablekit_settings_tests diff --git a/apps/portablekit/CMakeLists.txt b/apps/portablekit/CMakeLists.txt index 3ad6270..06e8718 100644 --- a/apps/portablekit/CMakeLists.txt +++ b/apps/portablekit/CMakeLists.txt @@ -31,6 +31,7 @@ set(app_sources src/keys_file.cpp src/launcher_ui.cpp src/process.cpp + src/response_file.cpp src/text_format.cpp) set(profile_index 0) diff --git a/apps/portablekit/src/corpus.cpp b/apps/portablekit/src/corpus.cpp index e05280e..ecdac24 100644 --- a/apps/portablekit/src/corpus.cpp +++ b/apps/portablekit/src/corpus.cpp @@ -2,6 +2,7 @@ #include "app_home.hpp" #include "process.hpp" +#include "response_file.hpp" #include "text_format.hpp" #include "corpus_abi.hpp" @@ -25,6 +26,10 @@ namespace portablekit::app { namespace { namespace fs = std::filesystem; + +// Past this many characters a command goes through a response file. Windows +// starts nothing longer than 32767; this leaves room for the program path. +constexpr std::size_t kLongCommandLine = 8000u; using Clock = std::chrono::steady_clock; // Exit codes (app_main.cpp lists them all). @@ -470,9 +475,16 @@ int compile_corpus(const GameRecord &game, const CompileOptions &options) { const bool current = fs::exists(object, time_ec) && fs::last_write_time(object, time_ec) >= fs::last_write_time(source, time_ec); if (!current) { - const ProcessResult result = - run_process(compile_arguments(command, options.opt_level, source, object, paths.generated), - paths.log); + // Compile lines are short; one that is not goes through a + // response file, as the link does. + std::vector arguments = + compile_arguments(command, options.opt_level, source, object, paths.generated); + if (command_line_length(arguments) > kLongCommandLine) { + fs::path rsp = object; + rsp += ".rsp"; + arguments = with_response_file(arguments, command.prefix.size(), rsp); + } + const ProcessResult result = run_process(arguments, paths.log); if (!result.started || result.exit_code != 0) { const std::lock_guard guard(status_lock); if (!failed) failure = result.started ? "Compiling " + source.filename().string() + " failed; see " + path_text(paths.log) + "." : result.error; @@ -501,7 +513,11 @@ int compile_corpus(const GameRecord &game, const CompileOptions &options) { const auto link_start = Clock::now(); fs::path partial = paths.library; partial += ".part"; - const ProcessResult linked = run_process(link_arguments(command, partial, objects), paths.log); + // Every object by full path is too long a command line for Windows once + // a game has a few hundred units (32767 characters): through a file. + const ProcessResult linked = run_process( + with_response_file(link_arguments(command, partial, objects), command.prefix.size(), paths.root / "link.rsp"), + paths.log); status.link_seconds = seconds_since(link_start); if (!linked.started || linked.exit_code != 0) return fail(kLinkFailed, linked.started ? "Linking failed; see " + path_text(paths.log) + "." : linked.error); diff --git a/apps/portablekit/src/response_file.cpp b/apps/portablekit/src/response_file.cpp new file mode 100644 index 0000000..b65960f --- /dev/null +++ b/apps/portablekit/src/response_file.cpp @@ -0,0 +1,42 @@ +#include "response_file.hpp" + +#include + +namespace portablekit::app { + +std::string response_file_text(const std::vector &arguments) { + std::string text; + for (const std::string &argument : arguments) { + text += '"'; + for (const char c : argument) { + if (c == '\\') text += '/'; + else if (c == '"') text += "\\\""; + else text += c; + } + text += "\"\n"; + } + return text; +} + +std::vector with_response_file(const std::vector &command, std::size_t keep, + const std::filesystem::path &path) { + if (command.size() <= keep) return command; + const std::vector rest(command.begin() + static_cast(keep), command.end()); + { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file) return command; + file << response_file_text(rest); + if (!file) return command; + } + std::vector result(command.begin(), command.begin() + static_cast(keep)); + result.push_back("@" + path.string()); + return result; +} + +std::size_t command_line_length(const std::vector &command) { + std::size_t length = 0u; + for (const std::string &argument : command) length += argument.size() + 3u; // quotes and a space + return length; +} + +} // namespace portablekit::app diff --git a/apps/portablekit/src/response_file.hpp b/apps/portablekit/src/response_file.hpp new file mode 100644 index 0000000..25cedb2 --- /dev/null +++ b/apps/portablekit/src/response_file.hpp @@ -0,0 +1,32 @@ +#pragma once + +// Long command lines through a response file. Windows refuses to start a +// process whose command line is longer than 32767 characters, and a large +// game's link lists hundreds of objects by full path: PSP2i (NPJH50332), +// 385 units, failed there with error 206. Compilers and linkers of every +// toolchain the app drives (clang, clang-cl, lld) read "@file" as the +// arguments in that file. + +#include +#include +#include + +namespace portablekit::app { + +// The file's text: one argument a line, each in double quotes, with +// backslashes written as forward slashes (every argument with a backslash +// is a Windows path, which accepts either) so that the GNU and the Windows +// rules for reading response files give the same arguments. +[[nodiscard]] std::string response_file_text(const std::vector &arguments); + +// `command` with everything after the first `keep` arguments (the program, +// usually) moved into a response file at `path`, written here. Returns the +// command unchanged if the file cannot be written. +[[nodiscard]] std::vector with_response_file(const std::vector &command, std::size_t keep, + const std::filesystem::path &path); + +// The length of the command line the arguments make, spaces and quotes +// included, as a program starting them would build it. +[[nodiscard]] std::size_t command_line_length(const std::vector &command); + +} // namespace portablekit::app diff --git a/tests/response_file_tests.cpp b/tests/response_file_tests.cpp new file mode 100644 index 0000000..f769bc7 --- /dev/null +++ b/tests/response_file_tests.cpp @@ -0,0 +1,91 @@ +// The desktop app's response files: a link of hundreds of objects by full +// path goes through "@file", and the file reads back as the same arguments. + +#include "response_file.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +int g_failures = 0; + +void check(bool condition, const char *what) { + std::printf("%s %s\n", condition ? "ok " : "FAIL", what); + if (!condition) ++g_failures; +} + +// Reads a response file the way GNU tools do: quoted arguments, a backslash +// escaping the next character. +std::vector read_gnu(const std::string &text) { + std::vector arguments; + std::string current; + bool quoted = false, any = false; + for (std::size_t i = 0; i < text.size(); ++i) { + const char c = text[i]; + if (c == '\\' && i + 1 < text.size()) { + current += text[++i]; + any = true; + } else if (c == '"') { + quoted = !quoted; + any = true; + } else if (!quoted && (c == ' ' || c == '\n' || c == '\t' || c == '\r')) { + if (any) arguments.push_back(current); + current.clear(); + any = false; + } else { + current += c; + any = true; + } + } + if (any) arguments.push_back(current); + return arguments; +} + +} // namespace + +int main() { + using namespace portablekit::app; + namespace fs = std::filesystem; + + std::vector command = {"C:\\PortableKit\\toolchain\\bin\\clang++.exe", "-shared", "-o", + "C:\\Users\\Someone\\Desktop\\PortableKit\\data\\cache\\GAME\\lib.dll.part"}; + for (int i = 0; i < 400; ++i) + command.push_back("C:\\Users\\Someone\\Desktop\\PortableKit\\data\\cache\\NPJH50332-3e376f74\\69f683e076b23b09-O0" + "\\objects\\generated_unit_" + std::to_string(i) + ".o"); + command.push_back("-DNAME=\"quoted value\""); + check(command_line_length(command) > 32767u, "four hundred objects by full path are past Windows' limit"); + + const fs::path file = fs::temp_directory_path() / "portablekit_response_file_test.rsp"; + const auto short_command = with_response_file(command, 1u, file); + check(short_command.size() == 2u && short_command[0] == command[0] && short_command[1] == "@" + file.string(), + "the program stays, everything else becomes @file"); + check(command_line_length(short_command) < 1000u, "and the command line is short"); + + std::ifstream in(file, std::ios::binary); + std::stringstream text; + text << in.rdbuf(); + const auto read = read_gnu(text.str()); + check(read.size() == command.size() - 1u, "the file holds every other argument"); + bool same = read.size() == command.size() - 1u; + for (std::size_t i = 0; same && i < read.size(); ++i) { + std::string expected = command[i + 1u]; + for (char &c : expected) + if (c == '\\') c = '/'; + same = read[i] == expected; + } + check(same, "and reads back as the same arguments, with forward slashes"); + check(text.str().find('\\') == std::string::npos || text.str().find("\\\"") != std::string::npos, + "no backslash is left but the quote escapes"); + fs::remove(file); + + const auto unchanged = with_response_file({"clang++"}, 1u, file); + check(unchanged.size() == 1u && !fs::exists(file), "a command with nothing to move is left alone"); + + if (g_failures != 0) std::printf("%d failed\n", g_failures); + return g_failures == 0 ? 0 : 1; +} From 3f6cb17777cbdf5fdc063ee075cf5abb0814ba23 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:37:31 +0300 Subject: [PATCH 14/26] Implement sceKernelReferEventFlagStatus, sceDisplayGetFramePerSec, sceMpegAvcDecodeFlush Three more calls God of War (UCES00842) makes on its way into the game, the first two also Patapon's (UCES00995): - ReferEventFlagStatus fills SceKernelEventFlagInfo as pspthreadman.h lays it out (size, name, attr, initPattern, currentPattern, numWaitThreads) up to the size the game gives; event flags now keep the pattern they were created with. - GetFramePerSec returns the refresh rate as a float in $f0, 60000/1001 (59.94). - AvcDecodeFlush drops the access unit taken and resets the decoder, as when a movie starts. NIDs match SHA-1 of the names. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 3 +++ host/hle/hle_media.cpp | 6 ++++++ host/hle/hle_mpeg.cpp | 13 +++++++++++++ host/hle/hle_threadman.cpp | 30 +++++++++++++++++++++++++++++- host/kernel/kernel.hpp | 1 + 5 files changed, 52 insertions(+), 1 deletion(-) diff --git a/configs/nids.csv b/configs/nids.csv index 6676347..1dd1856 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -99,6 +99,7 @@ sceDisplay,0x984C27E7,sceDisplayWaitVblankStart sceDisplay,0x9C6EAAD7,sceDisplayGetVcount sceDisplay,0x36CDFADE,sceDisplayWaitVblank sceDisplay,0xEEDA2E54,sceDisplayGetFrameBuf +sceDisplay,0xDBA6C4C4,sceDisplayGetFramePerSec sceDmac,0x617F3FE6,sceDmacMemcpy sceGe_user,0x03444EB4,sceGeListSync sceGe_user,0x05DB22CE,sceGeUnsetCallback @@ -190,6 +191,7 @@ sceMpeg,0xF5E7EA31,sceMpegAvcConvertToYuv420 sceMpeg,0xF8DCB679,sceMpegQueryAtracEsSize sceMpeg,0xFE246728,sceMpegGetAvcAu sceMpeg,0x31BD0272,sceMpegAvcCsc +sceMpeg,0x4571CC64,sceMpegAvcDecodeFlush sceNet,0x0BF0A3AE,sceNetGetLocalEtherAddr sceNet,0x281928A9,sceNetTerm sceNet,0x39AF39A6,sceNetInit @@ -456,6 +458,7 @@ ThreadManForUser,0xDF52098F,sceKernelTryReceiveMsgPipe ThreadManForUser,0x349B864D,sceKernelCancelMsgPipe ThreadManForUser,0x33BE4024,sceKernelReferMsgPipeStatus ThreadManForUser,0x17C1684E,sceKernelReferThreadStatus +ThreadManForUser,0xA66B0120,sceKernelReferEventFlagStatus UtilsForUser,0x27CC57F0,sceKernelLibcTime UtilsForUser,0x3EE30821,sceKernelDcacheWritebackRange UtilsForUser,0x6AD345D7,sceKernelSetGPO diff --git a/host/hle/hle_media.cpp b/host/hle/hle_media.cpp index 36f18ff..f1f4740 100644 --- a/host/hle/hle_media.cpp +++ b/host/hle/hle_media.cpp @@ -656,6 +656,12 @@ void register_vblank_waits(HleRegistrar &hle) { wait.type = WaitType::VBlank; kernel().block(ctx, wait, 0u); }); + // sceDisplayGetFramePerSec() -> float in $f0: the refresh rate, 60000/1001 + // (59.94) on a PSP. God of War (UCES00842) and Patapon (UCES00995) ask. + hle.add("sceDisplay", "sceDisplayGetFramePerSec", [](Runtime &, AllegrexContext &ctx) { + ctx.fpr[0] = 60000.0f / 1001.0f; + kernel().finish(ctx, 0u); + }); hle.add("sceDisplay", "sceDisplayGetVcount", [](Runtime &, AllegrexContext &ctx) { trace_pacing("sceDisplayGetVcount", static_cast(kernel().vblank_count())); kernel().finish(ctx, static_cast(kernel().vblank_count())); diff --git a/host/hle/hle_mpeg.cpp b/host/hle/hle_mpeg.cpp index 1c0aa48..22666ef 100644 --- a/host/hle/hle_mpeg.cpp +++ b/host/hle/hle_mpeg.cpp @@ -573,6 +573,19 @@ void register_decoding(HleRegistrar &hle) { if (arg(ctx, 3) != 0u) memory.store32(arg(ctx, 3), produced ? 1u : 0u); finish_traced(ctx, "sceMpegAvcDecodeYCbCr", 0u, produced ? "picture " + std::to_string(state->pictures) : "none"); }); + // sceMpegAvcDecodeFlush(mpeg): drops what the decoder holds, as before a + // seek or a new stream. God of War (UCES00842) calls it when a movie + // starts. + hle.add("sceMpeg", "sceMpegAvcDecodeFlush", [](Runtime &, AllegrexContext &ctx) { + MpegState *state = find_mpeg(arg(ctx, 0)); + if (state == nullptr) { + finish_traced(ctx, "sceMpegAvcDecodeFlush", mpeg_error::kInvalidValue, {}, 1u); + return; + } + state->video_unit.reset(); + state->video.reset(); + finish_traced(ctx, "sceMpegAvcDecodeFlush", 0u, {}, 1u); + }); // sceMpegAvcCsc(mpeg, ycbcr, range, frameWidth, dest): a picture from // sceMpegAvcDecodeYCbCr's buffer into pixels in the format // sceMpegAvcDecodeMode set, `frameWidth` pixels a row. `range` is diff --git a/host/hle/hle_threadman.cpp b/host/hle/hle_threadman.cpp index abfe547..949e2eb 100644 --- a/host/hle/hle_threadman.cpp +++ b/host/hle/hle_threadman.cpp @@ -396,10 +396,38 @@ void register_semaphores(HleRegistrar &hle) { void register_event_flags(HleRegistrar &hle) { hle.add("ThreadManForUser", "sceKernelCreateEventFlag", [](Runtime &rt, AllegrexContext &ctx) { const SceUID uid = kernel().allocate_uid(); - kernel().event_flags[uid] = EventFlag{read_cstring(rt.memory(), arg(ctx, 0), 32u), arg(ctx, 1), arg(ctx, 2), {}}; + kernel().event_flags[uid] = EventFlag{read_cstring(rt.memory(), arg(ctx, 0), 32u), arg(ctx, 1), arg(ctx, 2), {}, arg(ctx, 2)}; if (trace_sync()) log_sync("CreateEventFlag " + std::to_string(uid) + " " + kernel().event_flags[uid].name); kernel().finish(ctx, as_unsigned(uid)); }); + // sceKernelReferEventFlagStatus(uid, SceKernelEventFlagInfo *info): size, + // name[32], attr, initPattern, currentPattern, numWaitThreads, as in + // pspthreadman.h; written up to the size the game gives. God of War + // (UCES00842) and Patapon (UCES00995) read the current pattern. + hle.add("ThreadManForUser", "sceKernelReferEventFlagStatus", [](Runtime &rt, AllegrexContext &ctx) { + const auto found = kernel().event_flags.find(as_signed(arg(ctx, 0))); + if (found == kernel().event_flags.end()) { + kernel().finish(ctx, error::kUnknownEvfid); + return; + } + const EventFlag &flag = found->second; + auto &memory = rt.memory(); + const std::uint32_t info = arg(ctx, 1); + const std::uint32_t size = info != 0u ? memory.load32(info) : 0u; + std::array bytes{}; + const auto put = [&bytes](std::size_t at, std::uint32_t value) { + for (std::size_t i = 0; i < 4u; ++i) bytes[at + i] = static_cast(value >> (8u * i)); + }; + put(0u, size); + for (std::size_t i = 0; i < 31u && i < flag.name.size(); ++i) bytes[4u + i] = static_cast(flag.name[i]); + put(36u, flag.attributes); + put(40u, flag.initial_pattern); + put(44u, flag.pattern); + put(48u, static_cast(flag.waiters.size())); + for (std::uint32_t i = 0; i < std::min(size, static_cast(bytes.size())); ++i) + memory.store8(info + i, bytes[i]); + kernel().finish(ctx, 0u); + }); hle.add("ThreadManForUser", "sceKernelDeleteEventFlag", [](Runtime &, AllegrexContext &ctx) { auto found = kernel().event_flags.find(as_signed(arg(ctx, 0))); if (found == kernel().event_flags.end()) { diff --git a/host/kernel/kernel.hpp b/host/kernel/kernel.hpp index 24158d1..96a084c 100644 --- a/host/kernel/kernel.hpp +++ b/host/kernel/kernel.hpp @@ -154,6 +154,7 @@ struct EventFlag { std::uint32_t attributes{}; std::uint32_t pattern{}; std::deque waiters; + std::uint32_t initial_pattern{}; // for sceKernelReferEventFlagStatus }; struct Mutex { From 36e8073fd233897114b2aa0097c1acc325667cd7 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:37:31 +0300 Subject: [PATCH 15/26] Charge a busy thread's time as any import finishes, past 100 ms The busy clock (9df93c8) charged only on clock reads. God of War's movie player spins on sceMpegRingbufferAvailableSize, which reads no clock, waiting for its sound thread to use up audio; emulated time stood still there and the intro never went on. Kernel::finish, which every import that does not wait goes through, now charges instead. A thread is charged only after 100 ms of real time without any wait, so a game that computes a frame and waits for the vblank is never charged; it moves the clock by what ran, at most 250 ms at a time. _NO_BUSY_CLOCK still turns it off. With it God of War plays its intro and starts the first level. Co-Authored-By: Claude Opus 5.5 (1M context) --- host/hle/hle_threadman.cpp | 7 ------- host/kernel/kernel.cpp | 7 +++++-- host/kernel/kernel.hpp | 14 ++++++++------ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/host/hle/hle_threadman.cpp b/host/hle/hle_threadman.cpp index 949e2eb..04f4155 100644 --- a/host/hle/hle_threadman.cpp +++ b/host/hle/hle_threadman.cpp @@ -269,21 +269,14 @@ void register_threads(HleRegistrar &hle) { } void register_time(HleRegistrar &hle) { - // Each clock read charges the time a thread that never waits has run - // (Kernel::charge_busy_time): God of War (UCES00842) draws its first - // screens in a loop that reads sceKernelGetSystemTimeWide and never - // waits, and without it the clock stood still at 1.2 s for good. hle.add("ThreadManForUser", "sceKernelGetSystemTime", [](Runtime &rt, AllegrexContext &ctx) { - kernel().charge_busy_time(); store64(rt.memory(), arg(ctx, 0), kernel().now_us()); kernel().finish(ctx, 0u); }); hle.add("ThreadManForUser", "sceKernelGetSystemTimeWide", [](Runtime &, AllegrexContext &ctx) { - kernel().charge_busy_time(); kernel().finish64(ctx, kernel().now_us()); }); hle.add("ThreadManForUser", "sceKernelGetSystemTimeLow", [](Runtime &, AllegrexContext &ctx) { - kernel().charge_busy_time(); kernel().finish(ctx, static_cast(kernel().now_us())); }); // (SceKernelSysClock *clock, seconds *, microseconds *): the clock in diff --git a/host/kernel/kernel.cpp b/host/kernel/kernel.cpp index 8753767..cdea64d 100644 --- a/host/kernel/kernel.cpp +++ b/host/kernel/kernel.cpp @@ -322,6 +322,7 @@ void Kernel::switch_to(AllegrexContext &ctx, Thread &thread) { void Kernel::finish(AllegrexContext &ctx, std::uint32_t result) { ctx.set_gpr(2, result); if (interrupt_active_ || current_thread() == nullptr) return; + charge_busy_time(); if (interrupts_enabled_ && !pending_interrupts_.empty()) { interrupted_context_ = ctx; @@ -637,8 +638,10 @@ std::string Kernel::describe_threads() const { void Kernel::charge_busy_time() { static const bool off = portablekit::env("NO_BUSY_CLOCK") != nullptr; if (off || interrupt_active_ || current_thread() == nullptr) return; - constexpr std::int64_t kBusyChargeMinUs = 1000; - constexpr std::int64_t kBusyChargeMaxUs = 50000; + // A frame that computes and then waits for the vblank never gets here: + // only a thread that has gone this long without any wait is charged. + constexpr std::int64_t kBusyChargeMinUs = 100000; + constexpr std::int64_t kBusyChargeMaxUs = 250000; const auto now = std::chrono::steady_clock::now(); if (busy_thread_ != current_uid_ || busy_waits_ != waits_begun_) { busy_thread_ = current_uid_; diff --git a/host/kernel/kernel.hpp b/host/kernel/kernel.hpp index 96a084c..f6659bd 100644 --- a/host/kernel/kernel.hpp +++ b/host/kernel/kernel.hpp @@ -347,12 +347,14 @@ class Kernel { // Periodic hook from the runtime's starvation boundary. void on_starvation(AllegrexContext &ctx); - // A thread that reads the clock again and again without ever waiting is - // using CPU time the clock should show: a PSP's loop that draws, reads - // the time and draws again sees it move. Called by the clock reads, this - // moves emulated time on by the real time the thread has run since it - // last waited or was charged (at most kBusyChargeMaxUs at a time), once - // that is at least kBusyChargeMinUs. _NO_BUSY_CLOCK turns it off. + // A thread that calls into the system again and again without ever + // waiting is using CPU time the clock should show: a PSP's loop that + // draws and reads the time, or polls a ring buffer, sees it move. Called + // as every import finishes, this moves emulated time on by the real time + // the thread has run since it last waited or was charged (at most + // kBusyChargeMaxUs at a time), once that is at least kBusyChargeMinUs + // (100 ms: longer than any frame of a game that waits for the vblank). + // _NO_BUSY_CLOCK turns it off. void charge_busy_time(); // Guest entry stubs. From 791d2fbbb1b436c48c0d2b095921d223839155c9 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:58:28 +0300 Subject: [PATCH 16/26] Implement sceRtc's tick and calendar calls GetTick, SetTick, CompareTick, GetCurrentClock (with a time zone in minutes), GetDayOfWeek, GetDaysInMonth, IsLeapYear, CheckValid, GetTickResolution and TickAdd{Ticks,Microseconds,Seconds,Minutes,Hours, Days,Weeks}. A tick is microseconds since 0001-01-01 in the proleptic Gregorian calendar; kernel/rtc_time.* does the arithmetic and has unit tests (the Unix epoch at 719162 days, leap centuries, weekdays, round trips). Vice City Stories and Chinatown Wars import GetTick and CompareTick, Chinatown Wars GetCurrentClock; no game has been seen calling any of these yet, so each says UNVERIFIED on first use, and what an invalid date returns is not traced. NIDs match SHA-1 of the names. Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 10 ++++ cmake/PortableKit.cmake | 1 + configs/nids.csv | 16 +++++ host/hle/hle_system.cpp | 124 +++++++++++++++++++++++++++++++++++++++ host/kernel/rtc_time.cpp | 67 +++++++++++++++++++++ host/kernel/rtc_time.hpp | 34 +++++++++++ tests/rtc_tests.cpp | 40 +++++++++++++ 7 files changed, 292 insertions(+) create mode 100644 host/kernel/rtc_time.cpp create mode 100644 host/kernel/rtc_time.hpp create mode 100644 tests/rtc_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 40b8248..574ea56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -283,6 +283,16 @@ if(PSPRECOMP_BUILD_TESTS) add_test(NAME portablekit_response_file_tests COMMAND portablekit_response_file_tests) endif() +# sceRtc's calendar arithmetic. +if(PSPRECOMP_BUILD_TESTS) + add_executable(portablekit_rtc_tests + "${PORTABLEKIT_ROOT}/tests/rtc_tests.cpp" + "${PORTABLEKIT_ROOT}/host/kernel/rtc_time.cpp") + target_include_directories(portablekit_rtc_tests PRIVATE "${PORTABLEKIT_ROOT}/host") + set_target_properties(portablekit_rtc_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + add_test(NAME portablekit_rtc_tests COMMAND portablekit_rtc_tests) +endif() + # Each platform's settings defaults. if(PSPRECOMP_BUILD_TESTS) add_executable(portablekit_settings_tests diff --git a/cmake/PortableKit.cmake b/cmake/PortableKit.cmake index bf923c5..85ddd1e 100644 --- a/cmake/PortableKit.cmake +++ b/cmake/PortableKit.cmake @@ -47,6 +47,7 @@ set(PORTABLEKIT_HOST_SOURCES host/kernel/kernel.cpp host/kernel/fixed_pool.cpp host/kernel/message_pipe.cpp + host/kernel/rtc_time.cpp host/kernel/load_trace.cpp host/kernel/fast_loading.cpp host/kernel/load_detector.cpp diff --git a/configs/nids.csv b/configs/nids.csv index 1dd1856..79de6e9 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -272,6 +272,22 @@ scePower,0x737486F2,scePowerSetClockFrequency sceRtc,0xE7C27D1B,sceRtcGetCurrentClockLocalTime sceRtc,0x3F7AD767,sceRtcGetCurrentTick sceRtc,0x011F03C1,sceRtcGetAccumulativeTime +sceRtc,0x6FF40ACC,sceRtcGetTick +sceRtc,0x7ED29E40,sceRtcSetTick +sceRtc,0x9ED0AE87,sceRtcCompareTick +sceRtc,0x4CFA57B0,sceRtcGetCurrentClock +sceRtc,0x57726BC1,sceRtcGetDayOfWeek +sceRtc,0x05EF322C,sceRtcGetDaysInMonth +sceRtc,0x42307A17,sceRtcIsLeapYear +sceRtc,0x4B1B5E82,sceRtcCheckValid +sceRtc,0x44F45E05,sceRtcTickAddTicks +sceRtc,0x26D25A5D,sceRtcTickAddMicroseconds +sceRtc,0xF2A4AFE5,sceRtcTickAddSeconds +sceRtc,0xE6605BCA,sceRtcTickAddMinutes +sceRtc,0x26D7A24A,sceRtcTickAddHours +sceRtc,0xE51B4B7A,sceRtcTickAddDays +sceRtc,0xCF3A2CA8,sceRtcTickAddWeeks +sceRtc,0xC41C2853,sceRtcGetTickResolution sceSasCore,0x019B25EB,__sceSasSetADSR sceSasCore,0x07F58C24,__sceSasGetAllEnvelopeHeights sceSasCore,0x267A6DD2,__sceSasRevParam diff --git a/host/hle/hle_system.cpp b/host/hle/hle_system.cpp index e8ec33e..e3d9f8d 100644 --- a/host/hle/hle_system.cpp +++ b/host/hle/hle_system.cpp @@ -4,6 +4,7 @@ #include "../profile.hpp" #include "hle_common.hpp" +#include "kernel/rtc_time.hpp" #include "overlays.hpp" #include "psprecomp/common.hpp" @@ -13,8 +14,11 @@ #include #include #include +#include +#include namespace portablekit { +void register_rtc_calendar(HleRegistrar &hle); namespace { // Epoch the virtual PSP clock starts at, so guest wall time advances with it. @@ -192,6 +196,7 @@ void register_platform(HleRegistrar &hle) { if (arg(ctx, 0) != 0u) store64(rt.memory(), arg(ctx, 0), kUnixEpochTick + guest_unix_us()); kernel().finish(ctx, 0u); }); + register_rtc_calendar(hle); hle.add("sceImpose", "sceImposeSetLanguageMode", success); // (int *language, int *button): the console's, as GetSystemParamInt says. hle.add("sceImpose", "sceImposeGetLanguageMode", [](Runtime &rt, AllegrexContext &ctx) { @@ -234,6 +239,125 @@ void register_platform(HleRegistrar &hle) { } // namespace +// ScePspDateTime: year, month, day, hour, minute, second (16-bit each), then +// the microsecond (32-bit). +rtc::DateTime read_date(const psprecomp::GuestMemory &memory, std::uint32_t address) { + rtc::DateTime date; + date.year = memory.load16(address); + date.month = memory.load16(address + 2u); + date.day = memory.load16(address + 4u); + date.hour = memory.load16(address + 6u); + date.minute = memory.load16(address + 8u); + date.second = memory.load16(address + 10u); + date.microsecond = memory.load32(address + 12u); + return date; +} + +void write_date(psprecomp::GuestMemory &memory, std::uint32_t address, const rtc::DateTime &date) { + memory.store16(address, static_cast(date.year)); + memory.store16(address + 2u, static_cast(date.month)); + memory.store16(address + 4u, static_cast(date.day)); + memory.store16(address + 6u, static_cast(date.hour)); + memory.store16(address + 8u, static_cast(date.minute)); + memory.store16(address + 10u, static_cast(date.second)); + memory.store32(address + 12u, date.microsecond); +} + +std::uint64_t load_tick(const psprecomp::GuestMemory &memory, std::uint32_t address) { + return static_cast(memory.load32(address)) | static_cast(memory.load32(address + 4u)) << 32u; +} + +void rtc_unverified(const char *name) { + static std::set said; + if (said.insert(name).second) std::cerr << "[rtc] " << name << " (UNVERIFIED: no game traced yet)\n"; +} + +// A date that does not exist: what the library answers has not been traced. +constexpr std::uint32_t kRtcInvalidDate = 0xFFFFFFFFu; + +void register_rtc_calendar(HleRegistrar &hle) { + // sceRtcGetTick(const ScePspDateTime *, u64 *tick). Vice City Stories and + // Chinatown Wars import it and CompareTick, Chinatown Wars GetCurrentClock; + // none has been seen calling them yet. The calendar is rtc_time.hpp's, + // unit-tested. + hle.add("sceRtc", "sceRtcGetTick", [](Runtime &rt, AllegrexContext &ctx) { + rtc_unverified("sceRtcGetTick"); + const auto tick = rtc::to_tick(read_date(rt.memory(), arg(ctx, 0))); + if (!tick) { + kernel().finish(ctx, kRtcInvalidDate); + return; + } + if (arg(ctx, 1) != 0u) store64(rt.memory(), arg(ctx, 1), *tick); + kernel().finish(ctx, 0u); + }); + // sceRtcSetTick(ScePspDateTime *, const u64 *tick) + hle.add("sceRtc", "sceRtcSetTick", [](Runtime &rt, AllegrexContext &ctx) { + rtc_unverified("sceRtcSetTick"); + write_date(rt.memory(), arg(ctx, 0), rtc::from_tick(load_tick(rt.memory(), arg(ctx, 1)))); + kernel().finish(ctx, 0u); + }); + // sceRtcCompareTick(const u64 *a, const u64 *b): 1, 0 or -1. + hle.add("sceRtc", "sceRtcCompareTick", [](Runtime &rt, AllegrexContext &ctx) { + rtc_unverified("sceRtcCompareTick"); + const std::uint64_t a = load_tick(rt.memory(), arg(ctx, 0)); + const std::uint64_t b = load_tick(rt.memory(), arg(ctx, 1)); + kernel().finish(ctx, a > b ? 1u : a < b ? 0xFFFFFFFFu : 0u); + }); + // sceRtcGetCurrentClock(ScePspDateTime *, int minutes east of UTC). + hle.add("sceRtc", "sceRtcGetCurrentClock", [](Runtime &rt, AllegrexContext &ctx) { + rtc_unverified("sceRtcGetCurrentClock"); + const auto offset = static_cast(static_cast(arg(ctx, 1))) * 60 * + static_cast(rtc::kTicksPerSecond); + const auto tick = static_cast(static_cast(rtc::kUnixEpochTick + guest_unix_us()) + offset); + write_date(rt.memory(), arg(ctx, 0), rtc::from_tick(tick)); + kernel().finish(ctx, 0u); + }); + hle.add("sceRtc", "sceRtcGetDayOfWeek", [](Runtime &, AllegrexContext &ctx) { + rtc_unverified("sceRtcGetDayOfWeek"); + kernel().finish(ctx, static_cast(rtc::day_of_week(static_cast(arg(ctx, 0)), + static_cast(arg(ctx, 1)), + static_cast(arg(ctx, 2))))); + }); + hle.add("sceRtc", "sceRtcGetDaysInMonth", [](Runtime &, AllegrexContext &ctx) { + rtc_unverified("sceRtcGetDaysInMonth"); + kernel().finish(ctx, static_cast(rtc::days_in_month(static_cast(arg(ctx, 0)), + static_cast(arg(ctx, 1))))); + }); + hle.add("sceRtc", "sceRtcIsLeapYear", [](Runtime &, AllegrexContext &ctx) { + rtc_unverified("sceRtcIsLeapYear"); + kernel().finish(ctx, rtc::is_leap_year(static_cast(arg(ctx, 0))) ? 1u : 0u); + }); + hle.add("sceRtc", "sceRtcCheckValid", [](Runtime &rt, AllegrexContext &ctx) { + rtc_unverified("sceRtcCheckValid"); + kernel().finish(ctx, rtc::to_tick(read_date(rt.memory(), arg(ctx, 0))) ? 0u : kRtcInvalidDate); + }); + hle.add("sceRtc", "sceRtcGetTickResolution", [](Runtime &, AllegrexContext &ctx) { + rtc_unverified("sceRtcGetTickResolution"); + kernel().finish(ctx, static_cast(rtc::kTicksPerSecond)); + }); + // sceRtcTickAdd*(u64 *dst, const u64 *src, amount): the 64-bit amounts + // come in a2:a3, the 32-bit ones in a2. + const auto add_ticks = [](const char *name, std::uint64_t unit, bool wide) { + return [name, unit, wide](Runtime &rt, AllegrexContext &ctx) { + rtc_unverified(name); + const std::int64_t amount = + wide ? static_cast(static_cast(arg(ctx, 2)) | + static_cast(arg(ctx, 3)) << 32u) + : static_cast(static_cast(arg(ctx, 2))); + const std::uint64_t source = load_tick(rt.memory(), arg(ctx, 1)); + store64(rt.memory(), arg(ctx, 0), source + static_cast(amount * static_cast(unit))); + kernel().finish(ctx, 0u); + }; + }; + hle.add("sceRtc", "sceRtcTickAddTicks", add_ticks("sceRtcTickAddTicks", 1u, true)); + hle.add("sceRtc", "sceRtcTickAddMicroseconds", add_ticks("sceRtcTickAddMicroseconds", 1u, true)); + hle.add("sceRtc", "sceRtcTickAddSeconds", add_ticks("sceRtcTickAddSeconds", rtc::kTicksPerSecond, true)); + hle.add("sceRtc", "sceRtcTickAddMinutes", add_ticks("sceRtcTickAddMinutes", 60u * rtc::kTicksPerSecond, true)); + hle.add("sceRtc", "sceRtcTickAddHours", add_ticks("sceRtcTickAddHours", 3600u * rtc::kTicksPerSecond, false)); + hle.add("sceRtc", "sceRtcTickAddDays", add_ticks("sceRtcTickAddDays", rtc::kTicksPerDay, false)); + hle.add("sceRtc", "sceRtcTickAddWeeks", add_ticks("sceRtcTickAddWeeks", 7u * rtc::kTicksPerDay, false)); +} + void register_system(HleRegistrar &hle) { register_utils(hle); register_process(hle); diff --git a/host/kernel/rtc_time.cpp b/host/kernel/rtc_time.cpp new file mode 100644 index 0000000..ef88bbd --- /dev/null +++ b/host/kernel/rtc_time.cpp @@ -0,0 +1,67 @@ +#include "rtc_time.hpp" + +namespace portablekit::rtc { + +bool is_leap_year(int year) noexcept { return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } + +int days_in_month(int year, int month) noexcept { + static constexpr int kDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month < 1 || month > 12) return 0; + return kDays[month - 1] + (month == 2 && is_leap_year(year) ? 1 : 0); +} + +std::int64_t days_from_civil(int year, int month, int day) noexcept { + // Whole years, then whole months of this year, then days. + const std::int64_t y = year - 1; + std::int64_t days = y * 365 + y / 4 - y / 100 + y / 400; + for (int m = 1; m < month; ++m) days += days_in_month(year, m); + return days + (day - 1); +} + +int day_of_week(int year, int month, int day) noexcept { + // 0001-01-01 was a Monday. + return static_cast((days_from_civil(year, month, day) + 1) % 7); +} + +std::optional to_tick(const DateTime &date) noexcept { + if (date.year < 1 || date.year > 9999 || date.month < 1 || date.month > 12 || date.day < 1 || + date.day > days_in_month(date.year, date.month) || date.hour < 0 || date.hour > 23 || date.minute < 0 || + date.minute > 59 || date.second < 0 || date.second > 59 || date.microsecond >= 1'000'000u) + return std::nullopt; + const auto days = static_cast(days_from_civil(date.year, date.month, date.day)); + const std::uint64_t seconds = + static_cast(date.hour) * 3600u + static_cast(date.minute) * 60u + + static_cast(date.second); + return days * kTicksPerDay + seconds * kTicksPerSecond + date.microsecond; +} + +DateTime from_tick(std::uint64_t tick) noexcept { + DateTime date; + std::uint64_t days = tick / kTicksPerDay; + std::uint64_t rest = tick % kTicksPerDay; + // Whole 400-year cycles (146097 days), then years one by one. + date.year = 1 + static_cast(days / 146'097u) * 400; + days %= 146'097u; + for (;;) { + const std::uint64_t in_year = is_leap_year(date.year) ? 366u : 365u; + if (days < in_year) break; + days -= in_year; + ++date.year; + } + date.month = 1; + for (;;) { + const auto in_month = static_cast(days_in_month(date.year, date.month)); + if (days < in_month) break; + days -= in_month; + ++date.month; + } + date.day = static_cast(days) + 1; + date.microsecond = static_cast(rest % kTicksPerSecond); + rest /= kTicksPerSecond; + date.second = static_cast(rest % 60u); + date.minute = static_cast(rest / 60u % 60u); + date.hour = static_cast(rest / 3600u); + return date; +} + +} // namespace portablekit::rtc diff --git a/host/kernel/rtc_time.hpp b/host/kernel/rtc_time.hpp new file mode 100644 index 0000000..7f17132 --- /dev/null +++ b/host/kernel/rtc_time.hpp @@ -0,0 +1,34 @@ +#pragma once + +// The PSP's real-time clock arithmetic (sceRtc), with no guest memory: a +// tick is microseconds since 0001-01-01 00:00:00 in the proleptic Gregorian +// calendar, and a date-time is ScePspDateTime's fields. + +#include +#include + +namespace portablekit::rtc { + +struct DateTime { + int year{1}, month{1}, day{1}, hour{}, minute{}, second{}; + std::uint32_t microsecond{}; +}; + +inline constexpr std::uint64_t kTicksPerSecond = 1'000'000ull; +inline constexpr std::uint64_t kTicksPerDay = 86'400ull * kTicksPerSecond; +// 0001-01-01 to 1970-01-01. +inline constexpr std::uint64_t kUnixEpochTick = 719'162ull * kTicksPerDay; + +[[nodiscard]] bool is_leap_year(int year) noexcept; +// 0 for an invalid month. +[[nodiscard]] int days_in_month(int year, int month) noexcept; +// 0 Sunday .. 6 Saturday. +[[nodiscard]] int day_of_week(int year, int month, int day) noexcept; +// Days from 0001-01-01 to the date. +[[nodiscard]] std::int64_t days_from_civil(int year, int month, int day) noexcept; + +// Empty when a field is out of range (sceRtcCheckValid's checks). +[[nodiscard]] std::optional to_tick(const DateTime &date) noexcept; +[[nodiscard]] DateTime from_tick(std::uint64_t tick) noexcept; + +} // namespace portablekit::rtc diff --git a/tests/rtc_tests.cpp b/tests/rtc_tests.cpp new file mode 100644 index 0000000..19aa5ac --- /dev/null +++ b/tests/rtc_tests.cpp @@ -0,0 +1,40 @@ +// sceRtc's calendar arithmetic: ticks from 0001-01-01, dates, weekdays. + +#include "kernel/rtc_time.hpp" + +#include + +namespace { + +int g_failures = 0; + +void check(bool condition, const char *what) { + std::printf("%s %s\n", condition ? "ok " : "FAIL", what); + if (!condition) ++g_failures; +} + +} // namespace + +int main() { + using namespace portablekit::rtc; + check(to_tick({1970, 1, 1, 0, 0, 0, 0}) == kUnixEpochTick, "the Unix epoch is 719162 days in"); + check(to_tick({1, 1, 1, 0, 0, 0, 0}) == 0u, "tick 0 is 0001-01-01 00:00:00"); + const DateTime date{2008, 2, 29, 23, 59, 58, 123456}; + const auto tick = to_tick(date); + check(tick.has_value(), "a leap day is valid"); + const DateTime back = from_tick(*tick); + check(back.year == 2008 && back.month == 2 && back.day == 29 && back.hour == 23 && back.minute == 59 && + back.second == 58 && back.microsecond == 123456u, + "a tick converts back to the same date and time"); + check(!to_tick({2007, 2, 29, 0, 0, 0, 0}) && !to_tick({2008, 13, 1, 0, 0, 0, 0}) && + !to_tick({2008, 1, 1, 24, 0, 0, 0}), + "invalid dates are refused"); + check(day_of_week(1970, 1, 1) == 4 && day_of_week(2026, 9, 26) == 6 && day_of_week(1, 1, 1) == 1, + "weekdays: 1970-01-01 Thursday, 2026-09-26 Saturday, 0001-01-01 Monday"); + check(days_in_month(2000, 2) == 29 && days_in_month(1900, 2) == 28 && days_in_month(2026, 4) == 30, + "February in leap and non-leap centuries"); + const DateTime far = from_tick(*to_tick({9999, 12, 31, 12, 0, 0, 0})); + check(far.year == 9999 && far.month == 12 && far.day == 31 && far.hour == 12, "the last year the PSP allows"); + if (g_failures != 0) std::printf("%d failed\n", g_failures); + return g_failures == 0 ? 0 : 1; +} From b0b0443eea1f9cc4413b39d0d23b0c608efd5701 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:31:36 +0300 Subject: [PATCH 17/26] Implement sceNetAdhocctlCreate, Connect, Join, GetNameByAddr and sceNetEtherNtostr Joining a group straight through the ad hoc control library, as Vice City Stories and Chinatown Wars import it, rather than through the netconf dialog: Create and Connect name the group, Join takes an entry of the scan results. They join through the same relay client the netconf dialog uses; with no server set up the handlers get the timeout error event. GetNameByAddr answers the nickname of a peer in the group or of this player; EtherNtostr formats a MAC address. No game has been traced calling any of them yet: each says UNVERIFIED on first use. NIDs match SHA-1 of the names. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 2 ++ host/hle/hle_adhoc.cpp | 62 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/configs/nids.csv b/configs/nids.csv index 79de6e9..41163ad 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -196,6 +196,7 @@ sceNet,0x0BF0A3AE,sceNetGetLocalEtherAddr sceNet,0x281928A9,sceNetTerm sceNet,0x39AF39A6,sceNetInit sceNet,0x50647530,sceNetFreeThreadinfo +sceNet,0x89360950,sceNetEtherNtostr sceNetAdhoc,0x157E6225,sceNetAdhocPtpClose sceNetAdhoc,0x4DA4C788,sceNetAdhocPtpSend sceNetAdhoc,0x6F92741B,sceNetAdhocPdpCreate @@ -224,6 +225,7 @@ sceNetAdhocctl,0x9D689E13,sceNetAdhocctlTerm sceNetAdhocctl,0xE162CB14,sceNetAdhocctlGetPeerList sceNetAdhocctl,0xE26F226E,sceNetAdhocctlInit sceNetAdhocctl,0xEC0635C1,sceNetAdhocctlCreate +sceNetAdhocctl,0x8916C003,sceNetAdhocctlGetNameByAddr sceNetAdhocDiscover,0x52DE1B97,sceNetAdhocDiscoverUpdate sceNetAdhocDiscover,0x941B3877,sceNetAdhocDiscoverInitStart sceNetAdhocDiscover,0x944DDBC6,sceNetAdhocDiscoverGetStatus diff --git a/host/hle/hle_adhoc.cpp b/host/hle/hle_adhoc.cpp index 5b9595c..ab925d4 100644 --- a/host/hle/hle_adhoc.cpp +++ b/host/hle/hle_adhoc.cpp @@ -309,6 +309,18 @@ void register_net(HleRegistrar &hle) { if (arg(ctx, 0) != 0u) write_mac(rt.memory(), arg(ctx, 0), own_mac()); done(ctx, "sceNetGetLocalEtherAddr", 1, 0u, adhoc::format_mac(own_mac())); }); + // sceNetEtherNtostr(const mac *, char *out): "xx:xx:xx:xx:xx:xx". + hle.add("sceNet", "sceNetEtherNtostr", [](Runtime &rt, AllegrexContext &ctx) { + auto &memory = rt.memory(); + if (arg(ctx, 0) == 0u || arg(ctx, 1) == 0u) return done(ctx, "sceNetEtherNtostr", 2, err::kInvalidArg); + log_once("net-ether-ntostr", "[adhoc] sceNetEtherNtostr (UNVERIFIED: no game traced yet)"); + Mac mac{}; + for (std::size_t i = 0; i < mac.size(); ++i) mac[i] = memory.load8(arg(ctx, 0) + static_cast(i)); + char text[18]; + std::snprintf(text, sizeof(text), "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + write_cstring(memory, arg(ctx, 1), text, 18u); + done(ctx, "sceNetEtherNtostr", 2, 0u, text); + }); hle.add("sceWlanDrv", "sceWlanGetEtherAddr", [](Runtime &rt, AllegrexContext &ctx) { if (arg(ctx, 0) != 0u) write_mac(rt.memory(), arg(ctx, 0), own_mac()); done(ctx, "sceWlanGetEtherAddr", 1, 0u, adhoc::format_mac(own_mac())); @@ -393,6 +405,56 @@ void register_adhocctl(HleRegistrar &hle) { if (was_connected) notify_handlers(kEventDisconnect, 0u); done(ctx, "sceNetAdhocctlDisconnect", 0, 0u); }); + // Joining a group directly, without the netconf dialog: Create names a + // group to host, Connect a group to join or host, Join an entry of the + // scan results (SceNetAdhocctlScanInfo, the group name at +8). The relay + // server makes no difference between hosting and joining: the first in + // a group hosts it. The connect event comes to the handlers when the + // server confirms, as with netconf. No game has been traced using these. + const auto join_group = [](const char *name, const std::string &group) { + return [name, group](AllegrexContext &ctx) { + State &s = state(); + if (!s.ctl_initialized) return done(ctx, name, 1, err::kCtlNotInitialized); + log_once(std::string("adhocctl-") + name, std::string("[adhoc] ") + name + " (UNVERIFIED: no game traced yet)"); + if (!settings::current().adhoc || adhoc_server_address().empty()) { + Client::log("[adhoc] no server is set up (menu: Network); joining fails", true); + notify_handlers(kEventError, err::kCtlTimeout); + return done(ctx, name, 1, 0u, "group \"" + group + "\", no server"); + } + start_client(); + Client::get().join(group); + done(ctx, name, 1, 0u, "group \"" + group + "\""); + }; + }; + hle.add("sceNetAdhocctl", "sceNetAdhocctlCreate", [join_group](Runtime &rt, AllegrexContext &ctx) { + if (arg(ctx, 0) == 0u) return done(ctx, "sceNetAdhocctlCreate", 1, err::kCtlInvalidArg); + join_group("sceNetAdhocctlCreate", read_cstring(rt.memory(), arg(ctx, 0), 8u))(ctx); + }); + hle.add("sceNetAdhocctl", "sceNetAdhocctlConnect", [join_group](Runtime &rt, AllegrexContext &ctx) { + if (arg(ctx, 0) == 0u) return done(ctx, "sceNetAdhocctlConnect", 1, err::kCtlInvalidArg); + join_group("sceNetAdhocctlConnect", read_cstring(rt.memory(), arg(ctx, 0), 8u))(ctx); + }); + hle.add("sceNetAdhocctl", "sceNetAdhocctlJoin", [join_group](Runtime &rt, AllegrexContext &ctx) { + if (arg(ctx, 0) == 0u) return done(ctx, "sceNetAdhocctlJoin", 1, err::kCtlInvalidArg); + join_group("sceNetAdhocctlJoin", read_cstring(rt.memory(), arg(ctx, 0) + 8u, 8u))(ctx); + }); + // sceNetAdhocctlGetNameByAddr(const mac *, SceNetAdhocctlNickname *): the + // nickname of a peer in the group, or this player's own. + hle.add("sceNetAdhocctl", "sceNetAdhocctlGetNameByAddr", [](Runtime &rt, AllegrexContext &ctx) { + auto &memory = rt.memory(); + if (!state().ctl_initialized) return done(ctx, "sceNetAdhocctlGetNameByAddr", 2, err::kCtlNotInitialized); + if (arg(ctx, 0) == 0u || arg(ctx, 1) == 0u) return done(ctx, "sceNetAdhocctlGetNameByAddr", 2, err::kCtlInvalidArg); + log_once("adhocctl-getnamebyaddr", "[adhoc] sceNetAdhocctlGetNameByAddr (UNVERIFIED: no game traced yet)"); + Mac mac{}; + for (std::size_t i = 0; i < mac.size(); ++i) mac[i] = memory.load8(arg(ctx, 0) + static_cast(i)); + std::optional name; + if (mac == own_mac()) name = nickname(); + for (const auto &peer : Client::get().peers()) + if (peer.mac == mac) name = peer.nickname; + if (!name) return done(ctx, "sceNetAdhocctlGetNameByAddr", 2, err::kCtlIdNotFound, adhoc::format_mac(mac)); + write_cstring(memory, arg(ctx, 1), *name, 128u); + done(ctx, "sceNetAdhocctlGetNameByAddr", 2, 0u, adhoc::format_mac(mac) + " " + *name); + }); hle.add("sceNetAdhocctl", "sceNetAdhocctlGetPeerList", [](Runtime &rt, AllegrexContext &ctx) { auto &memory = rt.memory(); const std::uint32_t length_address = arg(ctx, 0); From 5712e249d52bb37a0d0beaaa9726fc8f76fb562d Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:49:06 +0300 Subject: [PATCH 18/26] Answer the clock, cache and thread calls PSP2i, GoW and Patapon make - scePowerSetClockFrequency and its aliases now remember the clocks, and scePowerGet{Cpu,Bus,Pll}ClockFrequencyInt and GetPllClockFrequencyFloat (in $f0) report them; a PSP starts at 222/111 MHz. 0xEBD177D6, which God of War, Patapon and Chinatown Wars call with 333, 333, 166, is the alias public NID lists call scePower_EBD177D6. - sceKernelDcacheWritebackInvalidateRange: nothing to do on the host (Phantasy Star Portable 2 Infinity calls it). - sceKernelReleaseWaitThread: the thread's wait ends with RELEASE_WAIT (0x800201AA); NOT_WAIT (0x800201A6) when it was not waiting (PSP2i). - sceRtcGetTime64_t: seconds since 1970 of a ScePspDateTime (PSP2i). - 0x342061E5, which GoW, Patapon and MHP2G call with 0x03070010, is sceKernelSetCompiledSdkVersion370 in public NID lists. Hash-derived NIDs match SHA-1 of the names; the two named from public lists are the randomized ones, and the arguments games pass fit them. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 9 +++++++ host/hle/hle_sysmem.cpp | 3 +++ host/hle/hle_system.cpp | 50 +++++++++++++++++++++++++++++++++++--- host/hle/hle_threadman.cpp | 19 +++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/configs/nids.csv b/configs/nids.csv index 41163ad..301bb2e 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -271,6 +271,11 @@ scePower,0x469989AD,scePowerSetClockFrequency630 scePower,0xA85880D0,scePowerCheckWlanCoexistenceClock scePower,0xDFA8BAF8,scePowerUnregisterCallback scePower,0x737486F2,scePowerSetClockFrequency +scePower,0xEBD177D6,scePower_EBD177D6 +scePower,0xFDB5BFE9,scePowerGetCpuClockFrequencyInt +scePower,0xBD681969,scePowerGetBusClockFrequencyInt +scePower,0x34F9C463,scePowerGetPllClockFrequencyInt +scePower,0xEA382A27,scePowerGetPllClockFrequencyFloat sceRtc,0xE7C27D1B,sceRtcGetCurrentClockLocalTime sceRtc,0x3F7AD767,sceRtcGetCurrentTick sceRtc,0x011F03C1,sceRtcGetAccumulativeTime @@ -290,6 +295,7 @@ sceRtc,0x26D7A24A,sceRtcTickAddHours sceRtc,0xE51B4B7A,sceRtcTickAddDays sceRtc,0xCF3A2CA8,sceRtcTickAddWeeks sceRtc,0xC41C2853,sceRtcGetTickResolution +sceRtc,0xE1C93E47,sceRtcGetTime64_t sceSasCore,0x019B25EB,__sceSasSetADSR sceSasCore,0x07F58C24,__sceSasGetAllEnvelopeHeights sceSasCore,0x267A6DD2,__sceSasRevParam @@ -386,6 +392,7 @@ SysMemUserForUser,0xB6D61D02,sceKernelFreePartitionMemory SysMemUserForUser,0xDB83A952,sceKernelGetMemoryBlockAddr SysMemUserForUser,0xF77D77CB,sceKernelSetCompilerVersion SysMemUserForUser,0xFE707FDF,sceKernelAllocMemoryBlock +SysMemUserForUser,0x342061E5,sceKernelSetCompiledSdkVersion370 ThreadManForUser,0x0D81716A,sceKernelPollMbx ThreadManForUser,0x19CFF145,sceKernelCreateLwMutex ThreadManForUser,0x1FB15A32,sceKernelSetEventFlag @@ -477,6 +484,7 @@ ThreadManForUser,0x349B864D,sceKernelCancelMsgPipe ThreadManForUser,0x33BE4024,sceKernelReferMsgPipeStatus ThreadManForUser,0x17C1684E,sceKernelReferThreadStatus ThreadManForUser,0xA66B0120,sceKernelReferEventFlagStatus +ThreadManForUser,0x2C34E053,sceKernelReleaseWaitThread UtilsForUser,0x27CC57F0,sceKernelLibcTime UtilsForUser,0x3EE30821,sceKernelDcacheWritebackRange UtilsForUser,0x6AD345D7,sceKernelSetGPO @@ -487,6 +495,7 @@ UtilsForUser,0x920F104A,sceKernelIcacheInvalidateAll UtilsForUser,0xB435DEC5,sceKernelDcacheWritebackInvalidateAll UtilsForUser,0xBFA98062,sceKernelDcacheInvalidateRange UtilsForUser,0xC2DF770E,sceKernelIcacheInvalidateRange +UtilsForUser,0x34B9FA9E,sceKernelDcacheWritebackInvalidateRange scePsmf,0x0BA514E5,scePsmfGetVideoInfo scePsmf,0x0C120E1D,scePsmfSpecifyStreamWithStreamTypeNumber scePsmf,0x1E6D9013,scePsmfSpecifyStreamWithStreamType diff --git a/host/hle/hle_sysmem.cpp b/host/hle/hle_sysmem.cpp index 2388a49..6fe5a1e 100644 --- a/host/hle/hle_sysmem.cpp +++ b/host/hle/hle_sysmem.cpp @@ -89,6 +89,9 @@ void register_sysmem(HleRegistrar &hle) { hle.add("SysMemUserForUser", "sceKernelSetCompilerVersion", success); hle.add("SysMemUserForUser", "sceKernelSetCompiledSdkVersion603_605", success); hle.add("SysMemUserForUser", "sceKernelSetCompiledSdkVersion", success); + // 0x342061E5: named after public NID lists; God of War, Patapon and + // MHP2G call it with 0x03070010, SDK 3.70. + hle.add("SysMemUserForUser", "sceKernelSetCompiledSdkVersion370", success); hle.add("SysMemUserForUser", "sceKernelPrintf", [](Runtime &rt, AllegrexContext &ctx) { std::cerr << "[guest] " << format_guest(rt.memory(), ctx, read_cstring(rt.memory(), arg(ctx, 0), 1024u)); kernel().finish(ctx, 0u); diff --git a/host/hle/hle_system.cpp b/host/hle/hle_system.cpp index e3d9f8d..a382834 100644 --- a/host/hle/hle_system.cpp +++ b/host/hle/hle_system.cpp @@ -34,8 +34,11 @@ std::uint64_t guest_unix_us() { return boot_unix_us() + kernel().now_us(); } void success(Runtime &, AllegrexContext &ctx) { kernel().finish(ctx, 0u); } void register_utils(HleRegistrar &hle) { + // The host has no data cache to keep coherent. Phantasy Star Portable 2 + // Infinity (NPJH50332) uses WritebackInvalidateRange. for (const char *name : {"sceKernelDcacheWritebackAll", "sceKernelDcacheWritebackInvalidateAll", - "sceKernelDcacheInvalidateRange", "sceKernelDcacheWritebackRange", "sceKernelSetGPO"}) + "sceKernelDcacheInvalidateRange", "sceKernelDcacheWritebackRange", + "sceKernelDcacheWritebackInvalidateRange", "sceKernelSetGPO"}) hle.add("UtilsForUser", name, success); // The guest flushes the instruction cache after copying code into an overlay // slot, which is the profile's cue to re-check which overlay is loaded. @@ -165,8 +168,38 @@ void register_platform(HleRegistrar &hle) { // Nothing here ever changes the power state, so there is nothing to stop // reporting; the slot is simply forgotten. hle.add("scePower", "scePowerUnregisterCallback", success); - hle.add("scePower", "scePowerSetClockFrequency630", success); - hle.add("scePower", "scePowerSetClockFrequency", success); + // The clocks the game asked for, reported back when it asks: a PSP + // starts at 222/111 MHz. Setting them changes nothing here. + struct Clocks { + std::uint32_t pll{222u}, cpu{222u}, bus{111u}; + }; + static Clocks clocks; + // scePowerSetClockFrequency(pll, cpu, bus) and its aliases: 0xEBD177D6 + // (its name in public NID lists is scePower_EBD177D6), which God of War, + // Patapon and Chinatown Wars call with 333, 333, 166. + const auto set_clocks = [](Runtime &, AllegrexContext &ctx) { + clocks.pll = arg(ctx, 0); + clocks.cpu = arg(ctx, 1); + clocks.bus = arg(ctx, 2); + kernel().finish(ctx, 0u); + }; + hle.add("scePower", "scePowerSetClockFrequency630", set_clocks); + hle.add("scePower", "scePowerSetClockFrequency", set_clocks); + hle.add("scePower", "scePower_EBD177D6", set_clocks); + hle.add("scePower", "scePowerGetCpuClockFrequencyInt", [](Runtime &, AllegrexContext &ctx) { + kernel().finish(ctx, clocks.cpu); + }); + hle.add("scePower", "scePowerGetBusClockFrequencyInt", [](Runtime &, AllegrexContext &ctx) { + kernel().finish(ctx, clocks.bus); + }); + hle.add("scePower", "scePowerGetPllClockFrequencyInt", [](Runtime &, AllegrexContext &ctx) { + kernel().finish(ctx, clocks.pll); + }); + // The float forms answer in $f0. + hle.add("scePower", "scePowerGetPllClockFrequencyFloat", [](Runtime &, AllegrexContext &ctx) { + ctx.fpr[0] = static_cast(clocks.pll); + kernel().finish(ctx, 0u); + }); hle.add("scePower", "scePowerCheckWlanCoexistenceClock", success); hle.add("sceRtc", "sceRtcGetCurrentClockLocalTime", [](Runtime &rt, AllegrexContext &ctx) { @@ -331,6 +364,17 @@ void register_rtc_calendar(HleRegistrar &hle) { rtc_unverified("sceRtcCheckValid"); kernel().finish(ctx, rtc::to_tick(read_date(rt.memory(), arg(ctx, 0))) ? 0u : kRtcInvalidDate); }); + // sceRtcGetTime64_t(const ScePspDateTime *, u64 *time): seconds since + // 1970-01-01. Phantasy Star Portable 2 Infinity (NPJH50332) calls it. + hle.add("sceRtc", "sceRtcGetTime64_t", [](Runtime &rt, AllegrexContext &ctx) { + const auto tick = rtc::to_tick(read_date(rt.memory(), arg(ctx, 0))); + if (!tick || *tick < rtc::kUnixEpochTick) { + kernel().finish(ctx, kRtcInvalidDate); + return; + } + if (arg(ctx, 1) != 0u) store64(rt.memory(), arg(ctx, 1), (*tick - rtc::kUnixEpochTick) / rtc::kTicksPerSecond); + kernel().finish(ctx, 0u); + }); hle.add("sceRtc", "sceRtcGetTickResolution", [](Runtime &, AllegrexContext &ctx) { rtc_unverified("sceRtcGetTickResolution"); kernel().finish(ctx, static_cast(rtc::kTicksPerSecond)); diff --git a/host/hle/hle_threadman.cpp b/host/hle/hle_threadman.cpp index 04f4155..5b163b9 100644 --- a/host/hle/hle_threadman.cpp +++ b/host/hle/hle_threadman.cpp @@ -131,6 +131,25 @@ void register_threads(HleRegistrar &hle) { }; hle.add("ThreadManForUser", "sceKernelSleepThread", sleep); hle.add("ThreadManForUser", "sceKernelSleepThreadCB", sleep_cb); + // sceKernelReleaseWaitThread(uid): a waiting thread stops waiting, its + // call answering SCE_KERNEL_ERROR_RELEASE_WAIT. Phantasy Star Portable 2 + // Infinity (NPJH50332) calls it. + hle.add("ThreadManForUser", "sceKernelReleaseWaitThread", [](Runtime &, AllegrexContext &ctx) { + constexpr std::uint32_t kReleaseWait = 0x800201AAu; + constexpr std::uint32_t kNotWait = 0x800201A6u; + Thread *thread = kernel().find_thread(as_signed(arg(ctx, 0))); + if (thread == nullptr) { + kernel().finish(ctx, error::kUnknownThid); + return; + } + if (thread->status != ThreadStatus::Waiting) { + kernel().finish(ctx, kNotWait); + return; + } + if (trace_sync()) log_sync("ReleaseWaitThread " + std::to_string(thread->uid) + " " + thread->name); + kernel().wake(*thread, kReleaseWait); + kernel().finish(ctx, 0u); + }); hle.add("ThreadManForUser", "sceKernelWakeupThread", [](Runtime &, AllegrexContext &ctx) { Thread *thread = kernel().find_thread(as_signed(arg(ctx, 0))); if (thread == nullptr) { From ce8198706a41650672c067b86ecd8812eade3aa3 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:51:21 +0300 Subject: [PATCH 19/26] Answer infrastructure networking as a PSP with no access point sceNetInet, sceNetApctl, sceNetResolver, sceHttp(s), sceSsl and the sceUtility network modules and browser dialog, which Chinatown Wars, Patapon and MHP2G import, were logging stubs answering 0: a game asking whether a download succeeded was told yes. hle_net_offline.cpp answers as a PSP with networking but no access point: the libraries initialise, sockets, resolvers, templates, connections and requests can be made, and nothing connects, resolves or downloads (connect fails with ENETUNREACH, send and recv with ENOTCONN, accept and recvfrom with EWOULDBLOCK, select finds nothing ready, the access point state is disconnected, resolving and HTTP requests fail). InetAddr and InetNtop do their conversions. The browser dialog opens and closes at once. No game has been seen calling these past initialising: each says UNVERIFIED on first use, and the library error values where none is documented are negative codes in the library's range. Hex-only NIDs resolved where SHA-1 of the name matched (Sendto, Recvfrom, Shutdown, Getsockopt, Getsockname, Select, GetErrno, InetNtop, the resolver calls, Save/LoadSystemCookie, Load/UnloadNetModule, HtmlViewer). Co-Authored-By: Claude Opus 5.5 (1M context) --- cmake/PortableKit.cmake | 1 + configs/nids.csv | 23 ++++ host/hle/hle_common.hpp | 1 + host/hle/hle_net_offline.cpp | 240 +++++++++++++++++++++++++++++++++++ host/system.cpp | 1 + 5 files changed, 266 insertions(+) create mode 100644 host/hle/hle_net_offline.cpp diff --git a/cmake/PortableKit.cmake b/cmake/PortableKit.cmake index 85ddd1e..488a891 100644 --- a/cmake/PortableKit.cmake +++ b/cmake/PortableKit.cmake @@ -62,6 +62,7 @@ set(PORTABLEKIT_HOST_SOURCES host/hle/hle_mpeg.cpp host/hle/hle_psmfplayer.cpp host/hle/hle_psmf.cpp + host/hle/hle_net_offline.cpp host/fonts/game_font.cpp host/hle/hle_font.cpp host/hle/hle_utility.cpp diff --git a/configs/nids.csv b/configs/nids.csv index 301bb2e..2dc592d 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -141,6 +141,8 @@ sceHttp,0xEDEEB999,sceHttpReadData sceHttp,0xF49934F6,sceHttpSetMallocFunction sceHttp,0xF9D8EB63,sceHttpsEnd sceHttp,0xFCF8C055,sceHttpDeleteTemplate +sceHttp,0x76D1363B,sceHttpSaveSystemCookie +sceHttp,0xF1657B22,sceHttpLoadSystemCookie sceImpose,0x36AA6E91,sceImposeSetLanguageMode sceImpose,0x72189C48,sceImposeSetUMDPopup sceImpose,0x24FD7BCF,sceImposeGetLanguageMode @@ -249,6 +251,8 @@ sceNetApctl,0x5963991B,sceNetApctlDelHandler sceNetApctl,0x8ABADD51,sceNetApctlAddHandler sceNetApctl,0xB3EDD0EC,sceNetApctlTerm sceNetApctl,0xE2F91F9B,sceNetApctlInit +sceNetApctl,0xCFB957C6,sceNetApctlConnect +sceNetApctl,0x5DEAC81B,sceNetApctlGetState sceNetInet,0x17943399,sceNetInetInit sceNetInet,0x1A33F9AE,sceNetInetBind sceNetInet,0x2FE71FE7,sceNetInetSetsockopt @@ -265,6 +269,14 @@ sceNetInet,0xCDA85C99,sceNetInetRecv sceNetInet,0xD10A1A7A,sceNetInetListen sceNetInet,0xDB094E1B,sceNetInetAccept sceNetInet,0xE247B6D6,sceNetInetGetpeername +sceNetInet,0x4CFE4E56,sceNetInetShutdown +sceNetInet,0x05038FC7,sceNetInetSendto +sceNetInet,0xC91142E4,sceNetInetRecvfrom +sceNetInet,0x4A114C7C,sceNetInetGetsockopt +sceNetInet,0x162E6FD5,sceNetInetGetsockname +sceNetInet,0x5BE8D595,sceNetInetSelect +sceNetInet,0xFBABE411,sceNetInetGetErrno +sceNetInet,0xD0792666,sceNetInetInetNtop sceOpenPSID,0xC69BEBCE,sceOpenPSIDGetOpenPSID scePower,0x04B7766E,scePowerRegisterCallback scePower,0x469989AD,scePowerSetClockFrequency630 @@ -375,6 +387,12 @@ sceUtility,0xC492F751,sceUtilityGameSharingInitStart sceUtility,0xD81957B7,sceUtilityScreenshotGetStatus sceUtility,0xEFC6F80F,sceUtilityGameSharingShutdownStart sceUtility,0xF9E0008C,sceUtilityScreenshotShutdownStart +sceUtility,0x1579A159,sceUtilityLoadNetModule +sceUtility,0x64D50C56,sceUtilityUnloadNetModule +sceUtility,0xCDC3AA41,sceUtilityHtmlViewerInitStart +sceUtility,0x05AFB9E4,sceUtilityHtmlViewerUpdate +sceUtility,0xBDA7D894,sceUtilityHtmlViewerGetStatus +sceUtility,0xF5CE1134,sceUtilityHtmlViewerShutdownStart sceWlanDrv,0x0C622081,sceWlanGetEtherAddr sceWlanDrv,0xD7763699,sceWlanGetSwitchState StdioForUser,0x172D316E,sceKernelStdin @@ -527,3 +545,8 @@ scePsmfPlayer,0xB9848A74,scePsmfPlayerGetAudioData scePsmfPlayer,0xDF089680,scePsmfPlayerGetPsmfInfo scePsmfPlayer,0xE792CD94,scePsmfPlayerReleasePsmf scePsmfPlayer,0xF8EF08A6,scePsmfPlayerGetCurrentStatus +sceNetResolver,0xF3370E61,sceNetResolverInit +sceNetResolver,0x6138194A,sceNetResolverTerm +sceNetResolver,0x94523E09,sceNetResolverDelete +sceNetResolver,0x244172AF,sceNetResolverCreate +sceNetResolver,0x224C5F44,sceNetResolverStartNtoA diff --git a/host/hle/hle_common.hpp b/host/hle/hle_common.hpp index 7ee4510..21638a3 100644 --- a/host/hle/hle_common.hpp +++ b/host/hle/hle_common.hpp @@ -84,6 +84,7 @@ void register_atrac(HleRegistrar &hle); void register_mpeg(HleRegistrar &hle); void register_psmfplayer(HleRegistrar &hle); void register_psmf(HleRegistrar &hle); +void register_net_offline(HleRegistrar &hle); void register_font(HleRegistrar &hle); void register_utility(HleRegistrar &hle, const std::filesystem::path &memory_stick); void register_savedata(HleRegistrar &hle, const std::filesystem::path &memory_stick); diff --git a/host/hle/hle_net_offline.cpp b/host/hle/hle_net_offline.cpp new file mode 100644 index 0000000..1872982 --- /dev/null +++ b/host/hle/hle_net_offline.cpp @@ -0,0 +1,240 @@ +// Infrastructure networking, answered as a PSP with no access point: the +// libraries initialise, but nothing connects, resolves or downloads. Games +// check for a network in many ways; these answers let them see "no network" +// and carry on, rather than stubs that claim success with nothing behind it. +// Ad hoc play is hle_adhoc.cpp; real sockets are not needed yet. +// +// No game has been seen calling these past initialising, so each says +// UNVERIFIED on its first call. The error values are negative codes in each +// library's range where its documentation gives none, and BSD errno values +// (ENETUNREACH, ENOTCONN, EWOULDBLOCK) for the socket calls. + +#include "../profile.hpp" +#include "hle_common.hpp" +#include "utility_dialog.hpp" + +#include "psprecomp/common.hpp" + +#include +#include +#include +#include +#include + +namespace portablekit { +namespace { + +// BSD errno values, as the PSP's network stack uses them. +constexpr std::uint32_t kEWouldBlock = 35u; +constexpr std::uint32_t kENetUnreach = 51u; +constexpr std::uint32_t kENotConn = 57u; + +// Not traced: failures in each library's error range. +constexpr std::uint32_t kApctlNotConnected = 0x80410A04u; +constexpr std::uint32_t kResolverFailed = 0x80410408u; +constexpr std::uint32_t kHttpNotConnected = 0x80431100u; + +struct NetState { + std::uint32_t errno_value{}; + std::uint32_t next_socket{1u}; + std::uint32_t next_handler{1u}; + std::uint32_t next_resolver{1u}; + std::uint32_t next_http_id{1u}; + DialogLifecycle html_viewer; +}; + +NetState &net() { + static NetState state; + return state; +} + +void first_call(const char *name) { + static std::set said; + if (said.insert(name).second) + std::cerr << "[net] " << name << " (UNVERIFIED: no game traced yet; answered as a PSP with no network)\n"; +} + +// A socket call that fails: -1, with errno set for sceNetInetGetErrno. +auto failing(const char *name, std::uint32_t errno_value) { + return [name, errno_value](Runtime &, AllegrexContext &ctx) { + first_call(name); + net().errno_value = errno_value; + kernel().finish(ctx, 0xFFFFFFFFu); + }; +} + +auto answering(const char *name, std::uint32_t result) { + return [name, result](Runtime &, AllegrexContext &ctx) { + first_call(name); + kernel().finish(ctx, result); + }; +} + +// An id for a template, connection or request: they exist, they only never +// connect. +auto new_http_id(const char *name) { + return [name](Runtime &, AllegrexContext &ctx) { + first_call(name); + kernel().finish(ctx, net().next_http_id++); + }; +} + +} // namespace + +void register_net_offline(HleRegistrar &hle) { + // sceNetInet: sockets can be made; nothing reaches anywhere. + for (const char *name : {"sceNetInetInit", "sceNetInetTerm"}) hle.add("sceNetInet", name, answering(name, 0u)); + hle.add("sceNetInet", "sceNetInetSocket", [](Runtime &, AllegrexContext &ctx) { + first_call("sceNetInetSocket"); + kernel().finish(ctx, net().next_socket++); + }); + for (const char *name : {"sceNetInetBind", "sceNetInetListen", "sceNetInetClose", "sceNetInetCloseWithRST", + "sceNetInetSetsockopt", "sceNetInetShutdown"}) + hle.add("sceNetInet", name, answering(name, 0u)); + hle.add("sceNetInet", "sceNetInetConnect", failing("sceNetInetConnect", kENetUnreach)); + hle.add("sceNetInet", "sceNetInetSend", failing("sceNetInetSend", kENotConn)); + hle.add("sceNetInet", "sceNetInetSendto", failing("sceNetInetSendto", kENetUnreach)); + hle.add("sceNetInet", "sceNetInetRecv", failing("sceNetInetRecv", kENotConn)); + hle.add("sceNetInet", "sceNetInetRecvfrom", failing("sceNetInetRecvfrom", kEWouldBlock)); + hle.add("sceNetInet", "sceNetInetAccept", failing("sceNetInetAccept", kEWouldBlock)); + hle.add("sceNetInet", "sceNetInetGetpeername", failing("sceNetInetGetpeername", kENotConn)); + // (socket, level, option, value *, length *): zero, four bytes long. + hle.add("sceNetInet", "sceNetInetGetsockopt", [](Runtime &rt, AllegrexContext &ctx) { + first_call("sceNetInetGetsockopt"); + if (arg(ctx, 3) != 0u) rt.memory().store32(arg(ctx, 3), 0u); + if (ctx.gpr[8] != 0u) rt.memory().store32(ctx.gpr[8], 4u); + kernel().finish(ctx, 0u); + }); + // (socket, sockaddr *, length *): an unbound IPv4 address. + hle.add("sceNetInet", "sceNetInetGetsockname", [](Runtime &rt, AllegrexContext &ctx) { + first_call("sceNetInetGetsockname"); + auto &memory = rt.memory(); + if (arg(ctx, 1) != 0u) { + for (std::uint32_t i = 0; i < 16u; ++i) memory.store8(arg(ctx, 1) + i, 0u); + memory.store8(arg(ctx, 1), 16u); // sin_len + memory.store8(arg(ctx, 1) + 1u, 2u); // AF_INET + } + if (arg(ctx, 2) != 0u) memory.store32(arg(ctx, 2), 16u); + kernel().finish(ctx, 0u); + }); + // Nothing is ever ready. + hle.add("sceNetInet", "sceNetInetSelect", answering("sceNetInetSelect", 0u)); + hle.add("sceNetInet", "sceNetInetGetErrno", [](Runtime &, AllegrexContext &ctx) { + kernel().finish(ctx, net().errno_value); + }); + hle.add("sceNetInet", "sceNetInetGetPspError", [](Runtime &, AllegrexContext &ctx) { + first_call("sceNetInetGetPspError"); + kernel().finish(ctx, net().errno_value == 0u ? 0u : 0x80410000u | net().errno_value); + }); + hle.add("sceNetInet", "sceNetInetGetTcpcbstat", answering("sceNetInetGetTcpcbstat", 0u)); + // sceNetInetInetAddr(const char *): a dotted quad in network byte order, + // 0xFFFFFFFF (INADDR_NONE) for anything else. + hle.add("sceNetInet", "sceNetInetInetAddr", [](Runtime &rt, AllegrexContext &ctx) { + first_call("sceNetInetInetAddr"); + const std::string text = read_cstring(rt.memory(), arg(ctx, 0), 64u); + unsigned a = 0, b = 0, c = 0, d = 0; + char tail = 0; + const int got = std::sscanf(text.c_str(), "%u.%u.%u.%u%c", &a, &b, &c, &d, &tail); + const bool valid = got == 4 && a < 256u && b < 256u && c < 256u && d < 256u; + kernel().finish(ctx, valid ? (a | b << 8u | c << 16u | d << 24u) : 0xFFFFFFFFu); + }); + // sceNetInetInetNtop(af, const void *src, char *dst, size): IPv4 only. + hle.add("sceNetInet", "sceNetInetInetNtop", [](Runtime &rt, AllegrexContext &ctx) { + first_call("sceNetInetInetNtop"); + auto &memory = rt.memory(); + const std::uint32_t address = memory.load32(arg(ctx, 1)); + char text[16]; + std::snprintf(text, sizeof(text), "%u.%u.%u.%u", address & 0xFFu, address >> 8u & 0xFFu, address >> 16u & 0xFFu, + address >> 24u); + const std::string value = text; + if (arg(ctx, 0) != 2u || arg(ctx, 3) <= value.size()) { + kernel().finish(ctx, 0u); + return; + } + write_cstring(memory, arg(ctx, 2), value, arg(ctx, 3)); + kernel().finish(ctx, arg(ctx, 2)); + }); + + // sceNetApctl: no access point is ever joined. + for (const char *name : {"sceNetApctlInit", "sceNetApctlTerm", "sceNetApctlDelHandler", "sceNetApctlDisconnect"}) + hle.add("sceNetApctl", name, answering(name, 0u)); + hle.add("sceNetApctl", "sceNetApctlAddHandler", [](Runtime &, AllegrexContext &ctx) { + first_call("sceNetApctlAddHandler"); + kernel().finish(ctx, net().next_handler++); + }); + hle.add("sceNetApctl", "sceNetApctlGetInfo", answering("sceNetApctlGetInfo", kApctlNotConnected)); + hle.try_add("sceNetApctl", "sceNetApctlConnect", answering("sceNetApctlConnect", kApctlNotConnected)); + // (int *state): PSP_NET_APCTL_STATE_DISCONNECTED. + hle.try_add("sceNetApctl", "sceNetApctlGetState", [](Runtime &rt, AllegrexContext &ctx) { + first_call("sceNetApctlGetState"); + if (arg(ctx, 0) != 0u) rt.memory().store32(arg(ctx, 0), 0u); + kernel().finish(ctx, 0u); + }); + + // sceNetResolver: resolvers can be made; no name resolves. + for (const char *name : {"sceNetResolverInit", "sceNetResolverTerm", "sceNetResolverDelete"}) + hle.add("sceNetResolver", name, answering(name, 0u)); + hle.add("sceNetResolver", "sceNetResolverCreate", [](Runtime &rt, AllegrexContext &ctx) { + first_call("sceNetResolverCreate"); + if (arg(ctx, 0) != 0u) rt.memory().store32(arg(ctx, 0), net().next_resolver++); + kernel().finish(ctx, 0u); + }); + hle.add("sceNetResolver", "sceNetResolverStartNtoA", answering("sceNetResolverStartNtoA", kResolverFailed)); + + // sceHttp: templates, connections and requests can be made; sending + // fails. + for (const char *name : + {"sceHttpInit", "sceHttpEnd", "sceHttpsInit", "sceHttpsEnd", "sceHttpsLoadDefaultCert", "sceHttpSetMallocFunction", + "sceHttpSaveSystemCookie", "sceHttpLoadSystemCookie", "sceHttpSetRecvTimeOut", "sceHttpSetSendTimeOut", + "sceHttpSetConnectTimeOut", "sceHttpDisableCookie", "sceHttpDisableCache", "sceHttpDisableAuth", + "sceHttpAddExtraHeader", "sceHttpSetRedirectCallback", "sceHttpAbortRequest", "sceHttpDeleteTemplate", + "sceHttpDeleteConnection", "sceHttpDeleteRequest"}) + hle.add("sceHttp", name, answering(name, 0u)); + for (const char *name : {"sceHttpCreateTemplate", "sceHttpCreateConnectionWithURL", "sceHttpCreateRequestWithURL"}) + hle.add("sceHttp", name, new_http_id(name)); + for (const char *name : {"sceHttpSendRequest", "sceHttpReadData", "sceHttpGetStatusCode", "sceHttpGetContentLength", + "sceHttpGetAllHeader"}) + hle.add("sceHttp", name, answering(name, kHttpNotConnected)); + // (request, int *error) + hle.add("sceHttp", "sceHttpGetNetworkPspError", [](Runtime &rt, AllegrexContext &ctx) { + first_call("sceHttpGetNetworkPspError"); + if (arg(ctx, 1) != 0u) rt.memory().store32(arg(ctx, 1), kHttpNotConnected); + kernel().finish(ctx, 0u); + }); + // (request, int *error, unsigned *detail) + hle.add("sceHttp", "sceHttpsGetSslError", [](Runtime &rt, AllegrexContext &ctx) { + first_call("sceHttpsGetSslError"); + if (arg(ctx, 1) != 0u) rt.memory().store32(arg(ctx, 1), 0u); + if (arg(ctx, 2) != 0u) rt.memory().store32(arg(ctx, 2), 0u); + kernel().finish(ctx, 0u); + }); + for (const char *name : {"sceSslInit", "sceSslEnd"}) hle.add("sceSsl", name, answering(name, 0u)); + + // sceUtility: the network modules load; the browser dialog opens and + // closes at once, having shown nothing. + for (const char *name : {"sceUtilityLoadNetModule", "sceUtilityUnloadNetModule"}) + hle.add("sceUtility", name, answering(name, 0u)); + hle.add("sceUtility", "sceUtilityHtmlViewerInitStart", [](Runtime &rt, AllegrexContext &ctx) { + first_call("sceUtilityHtmlViewerInitStart"); + if (net().html_viewer.active()) { + kernel().finish(ctx, kErrorUtilityInvalidStatus); + return; + } + if (arg(ctx, 0) != 0u) rt.memory().store32(arg(ctx, 0) + dialog_common::kResultOffset, 0u); + net().html_viewer.start(); + kernel().finish(ctx, 0u); + }); + hle.add("sceUtility", "sceUtilityHtmlViewerUpdate", [](Runtime &, AllegrexContext &ctx) { + (void)net().html_viewer.poll(); + kernel().finish(ctx, 0u); + }); + hle.add("sceUtility", "sceUtilityHtmlViewerGetStatus", [](Runtime &, AllegrexContext &ctx) { + kernel().finish(ctx, net().html_viewer.poll()); + }); + hle.add("sceUtility", "sceUtilityHtmlViewerShutdownStart", [](Runtime &, AllegrexContext &ctx) { + (void)net().html_viewer.shutdown(); + kernel().finish(ctx, 0u); + }); +} + +} // namespace portablekit diff --git a/host/system.cpp b/host/system.cpp index f3fe0c5..5c49dbd 100644 --- a/host/system.cpp +++ b/host/system.cpp @@ -91,6 +91,7 @@ void install_system(Runtime &runtime, const psprecomp::Elf32Image &elf, const Pr register_mpeg(hle); register_psmfplayer(hle); register_psmf(hle); + register_net_offline(hle); register_font(hle); register_utility(hle, paths.memory_stick); register_adhoc(hle); From 05c6e4151929b71a7b6b4078451c305295fac01e Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:15:10 +0300 Subject: [PATCH 20/26] Implement the small calls left in the maintainer's games' import lists - sceIoMkdir, sceIoRmdir, sceIoRemove on the memory stick (a host folder); the disc is read-only (God of War, Vice City Stories). - sceUmdDeactivate: the disc is never taken away (MHP2G). - sceDisplayGetFrameBuf reports what SetFrameBuf was given; GetCurrentHcount and IsVblank from emulated time since the last vblank, 286 lines a frame (Chinatown Wars, Vice City Stories). - sceAudioOutput2ChangeLength sets the output's block size (God of War). - sceKernelPowerLock and PowerUnlock: nothing to hold off. - sceNpDrmSetLicenseeKey and sceNpDrmEdataSetupKey (Chinatown Wars). - sceUtilityGetSystemParamString: the nickname (id 1). - sceKernelGetThreadStackFreeSize and ThreadManForUser's sceKernelCheckThreadStack: the space below the stack pointer. - sceAtracGetInternalErrorInfo: no decoder error is kept. - sceNetAdhocDiscoverRequestSuspend: nothing to suspend (MHP2G). None has been seen called by a game yet except PowerLock/Unlock (God of War, PSP2i); the others say UNVERIFIED on first use where their answer is more than 0. NIDs match SHA-1 of the names. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 14 ++++++++++++++ host/hle/hle_adhoc.cpp | 6 ++++++ host/hle/hle_atrac.cpp | 12 ++++++++++++ host/hle/hle_io.cpp | 31 +++++++++++++++++++++++++++++++ host/hle/hle_media.cpp | 33 +++++++++++++++++++++++++++++++++ host/hle/hle_system.cpp | 25 +++++++++++++++++++++++++ host/hle/hle_threadman.cpp | 20 ++++++++++++++++++++ 7 files changed, 141 insertions(+) diff --git a/configs/nids.csv b/configs/nids.csv index 2dc592d..e55904d 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -30,6 +30,9 @@ IoFileMgrForUser,0xE3EB004C,sceIoDread IoFileMgrForUser,0xEB092469,sceIoDclose IoFileMgrForUser,0xB8A740F4,sceIoChstat IoFileMgrForUser,0x55F4717D,sceIoChdir +IoFileMgrForUser,0x06A70004,sceIoMkdir +IoFileMgrForUser,0x1117C65F,sceIoRmdir +IoFileMgrForUser,0xF27A9C51,sceIoRemove Kernel_Library,0x092968F4,sceKernelCpuSuspendIntr Kernel_Library,0x15B6446B,sceKernelUnlockLwMutex Kernel_Library,0x1839852A,sceKernelMemcpy @@ -72,6 +75,7 @@ sceAtrac3plus,0x0FAE370E,sceAtracSetHalfwayBufferAndGetID sceAtrac3plus,0x36FAABFB,sceAtracGetNextSample sceAtrac3plus,0x7DB31251,sceAtracAddStreamData sceAtrac3plus,0x83E85EA0,sceAtracGetSecondBufferInfo +sceAtrac3plus,0xE88F759B,sceAtracGetInternalErrorInfo sceAudio,0x01562BA3,sceAudioOutput2Reserve sceAudio,0x136CAF51,sceAudioOutputBlocking sceAudio,0x13F592BC,sceAudioOutputPannedBlocking @@ -84,6 +88,7 @@ sceAudio,0xB011922F,sceAudioGetChannelRestLength sceAudio,0xB7E1D8E7,sceAudioChangeChannelVolume sceAudio,0xCB2E439E,sceAudioSetChannelDataLen sceAudio,0xE2D56B2D,sceAudioOutputPanned +sceAudio,0x63F2889C,sceAudioOutput2ChangeLength sceCtrl,0x1F4011E6,sceCtrlSetSamplingMode sceCtrl,0x1F803938,sceCtrlReadBufferPositive sceCtrl,0x6A2774F3,sceCtrlSetSamplingCycle @@ -100,6 +105,8 @@ sceDisplay,0x9C6EAAD7,sceDisplayGetVcount sceDisplay,0x36CDFADE,sceDisplayWaitVblank sceDisplay,0xEEDA2E54,sceDisplayGetFrameBuf sceDisplay,0xDBA6C4C4,sceDisplayGetFramePerSec +sceDisplay,0x773DD3A3,sceDisplayGetCurrentHcount +sceDisplay,0x4D4E10EC,sceDisplayIsVblank sceDmac,0x617F3FE6,sceDmacMemcpy sceGe_user,0x03444EB4,sceGeListSync sceGe_user,0x05DB22CE,sceGeUnsetCallback @@ -233,6 +240,7 @@ sceNetAdhocDiscover,0x941B3877,sceNetAdhocDiscoverInitStart sceNetAdhocDiscover,0x944DDBC6,sceNetAdhocDiscoverGetStatus sceNetAdhocDiscover,0xA2246614,sceNetAdhocDiscoverTerm sceNetAdhocDiscover,0xF7D13214,sceNetAdhocDiscoverStop +sceNetAdhocDiscover,0xA423A21B,sceNetAdhocDiscoverRequestSuspend sceNetAdhocMatching,0x2A2A1E07,sceNetAdhocMatchingInit sceNetAdhocMatching,0x32B156B3,sceNetAdhocMatchingStop sceNetAdhocMatching,0x5E3D4B79,sceNetAdhocMatchingSelectTarget @@ -341,6 +349,8 @@ sceSuspendForUser,0x090CCB3F,sceKernelPowerTick sceSuspendForUser,0x3E0271D3,sceKernelVolatileMemLock sceSuspendForUser,0xA14F40B2,sceKernelVolatileMemTryLock sceSuspendForUser,0xA569E425,sceKernelVolatileMemUnlock +sceSuspendForUser,0xEADB1BD7,sceKernelPowerLock +sceSuspendForUser,0x3AEE7261,sceKernelPowerUnlock sceUmdUser,0x20628E6F,sceUmdGetErrorStat sceUmdUser,0x46EBB729,sceUmdCheckMedium sceUmdUser,0x4A9E5E29,sceUmdWaitDriveStatCB @@ -351,6 +361,7 @@ sceUmdUser,0xAEE7404D,sceUmdRegisterUMDCallBack sceUmdUser,0xBD2BDE07,sceUmdUnRegisterUMDCallBack sceUmdUser,0xC6183D47,sceUmdActivate sceUmdUser,0x6AF9B50A,sceUmdCancelWaitDriveStat +sceUmdUser,0xE83742BA,sceUmdDeactivate sceUtility,0x180F7B62,sceUtilityGamedataInstallAbort sceUtility,0x24AC31EB,sceUtilityGamedataInstallInitStart sceUtility,0x2A2B3DE0,sceUtilityLoadModule @@ -503,6 +514,7 @@ ThreadManForUser,0x33BE4024,sceKernelReferMsgPipeStatus ThreadManForUser,0x17C1684E,sceKernelReferThreadStatus ThreadManForUser,0xA66B0120,sceKernelReferEventFlagStatus ThreadManForUser,0x2C34E053,sceKernelReleaseWaitThread +ThreadManForUser,0xD13BDE95,sceKernelCheckThreadStack UtilsForUser,0x27CC57F0,sceKernelLibcTime UtilsForUser,0x3EE30821,sceKernelDcacheWritebackRange UtilsForUser,0x6AD345D7,sceKernelSetGPO @@ -550,3 +562,5 @@ sceNetResolver,0x6138194A,sceNetResolverTerm sceNetResolver,0x94523E09,sceNetResolverDelete sceNetResolver,0x244172AF,sceNetResolverCreate sceNetResolver,0x224C5F44,sceNetResolverStartNtoA +scePspNpDrm_user,0xA1336091,sceNpDrmSetLicenseeKey +scePspNpDrm_user,0x08D98894,sceNpDrmEdataSetupKey diff --git a/host/hle/hle_adhoc.cpp b/host/hle/hle_adhoc.cpp index ab925d4..fcc7fc2 100644 --- a/host/hle/hle_adhoc.cpp +++ b/host/hle/hle_adhoc.cpp @@ -309,6 +309,12 @@ void register_net(HleRegistrar &hle) { if (arg(ctx, 0) != 0u) write_mac(rt.memory(), arg(ctx, 0), own_mac()); done(ctx, "sceNetGetLocalEtherAddr", 1, 0u, adhoc::format_mac(own_mac())); }); + // sceNetAdhocDiscoverRequestSuspend(): nothing is being discovered to + // suspend. MHP2G imports it. + hle.try_add("sceNetAdhocDiscover", "sceNetAdhocDiscoverRequestSuspend", [](Runtime &, AllegrexContext &ctx) { + log_once("adhoc-discover-suspend", "[adhoc] sceNetAdhocDiscoverRequestSuspend (UNVERIFIED: no game traced yet)"); + kernel().finish(ctx, 0u); + }); // sceNetEtherNtostr(const mac *, char *out): "xx:xx:xx:xx:xx:xx". hle.add("sceNet", "sceNetEtherNtostr", [](Runtime &rt, AllegrexContext &ctx) { auto &memory = rt.memory(); diff --git a/host/hle/hle_atrac.cpp b/host/hle/hle_atrac.cpp index 0a75a07..4129deb 100644 --- a/host/hle/hle_atrac.cpp +++ b/host/hle/hle_atrac.cpp @@ -761,6 +761,18 @@ void register_atrac_functions(HleRegistrar &hle) { << " from=" << write.file_offset; finish_traced(ctx, "sceAtracGetStreamDataInfo", 0u, details.str()); }); + // sceAtracGetInternalErrorInfo(id, int *error): the decoder's last error; + // none is ever kept here. Chinatown Wars, Vice City Stories and God of War + // import it. + hle.add("sceAtrac3plus", "sceAtracGetInternalErrorInfo", [](Runtime &rt, AllegrexContext &ctx) { + const std::uint32_t id = arg(ctx, 0); + if (find_context(id) == nullptr) { + finish_traced(ctx, "sceAtracGetInternalErrorInfo", context_error(id)); + return; + } + write_s32(rt.memory(), arg(ctx, 1), 0); + finish_traced(ctx, "sceAtracGetInternalErrorInfo", 0u); + }); // sceAtracGetSecondBufferInfo(id, outPosition, outBytes): where a // looping streamed track wants the file's end kept, when its buffer // cannot hold it. Tracks loop here from the bytes the game added the diff --git a/host/hle/hle_io.cpp b/host/hle/hle_io.cpp index 165ba5a..4ab1499 100644 --- a/host/hle/hle_io.cpp +++ b/host/hle/hle_io.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace portablekit { @@ -784,6 +785,34 @@ void register_io(HleRegistrar &hle, const std::filesystem::path &disc_image, con hle.add("IoFileMgrForUser", "sceIoDclose", [](Runtime &, AllegrexContext &ctx) { kernel().finish(ctx, io().files.erase(arg(ctx, 0)) != 0u ? 0u : io_error::kBadFileDescriptor); }); + // sceIoMkdir(path, mode), sceIoRmdir(path), sceIoRemove(path): on the + // memory stick, which is a host folder; the disc is read-only. God of + // War and Vice City Stories import them; no game has been seen calling + // them yet. + const auto stick_op = [](const char *name, int op) { + return [name, op](Runtime &rt, AllegrexContext &ctx) { + static std::set said; + if (said.insert(name).second) std::cerr << "[io] " << name << " (UNVERIFIED: no game traced yet)\n"; + const std::string path = read_cstring(rt.memory(), arg(ctx, 0), 256u); + const SplitPath split = split_path(path); + if (split.device != Device::MemoryStick) { + kernel().finish(ctx, split.device == Device::Disc ? io_error::kReadOnly : io_error::kDeviceNotFound); + return; + } + std::error_code ec; + const auto host = host_path(split.path); + bool ok = false; + if (op == 0) ok = std::filesystem::create_directory(host, ec) && !ec; + else if (op == 1) ok = std::filesystem::is_directory(host, ec) && std::filesystem::is_empty(host, ec) && + std::filesystem::remove(host, ec); + else ok = std::filesystem::is_regular_file(host, ec) && std::filesystem::remove(host, ec); + if (trace_io()) std::cerr << "[io] " << name << " " << path << (ok ? " ok" : " failed") << "\n"; + kernel().finish(ctx, ok ? 0u : io_error::kFileNotFound); + }; + }; + hle.add("IoFileMgrForUser", "sceIoMkdir", stick_op("sceIoMkdir", 0)); + hle.add("IoFileMgrForUser", "sceIoRmdir", stick_op("sceIoRmdir", 1)); + hle.add("IoFileMgrForUser", "sceIoRemove", stick_op("sceIoRemove", 2)); hle.add("IoFileMgrForUser", "sceIoRename", [](Runtime &rt, AllegrexContext &ctx) { const SplitPath from = split_path(read_cstring(rt.memory(), arg(ctx, 0), 256u)); const SplitPath to = split_path(read_cstring(rt.memory(), arg(ctx, 1), 256u)); @@ -915,6 +944,8 @@ void register_io(HleRegistrar &hle, const std::filesystem::path &disc_image, con }); hle.add("sceUmdUser", "sceUmdActivate", [](Runtime &, AllegrexContext &ctx) { kernel().finish(ctx, 0u); }); + // The disc is never taken away, so letting go of it changes nothing. + hle.add("sceUmdUser", "sceUmdDeactivate", [](Runtime &, AllegrexContext &ctx) { kernel().finish(ctx, 0u); }); hle.add("sceUmdUser", "sceUmdGetDriveStat", [](Runtime &, AllegrexContext &ctx) { kernel().finish(ctx, drive_status()); }); hle.add("sceUmdUser", "sceUmdGetErrorStat", [](Runtime &, AllegrexContext &ctx) { kernel().finish(ctx, 0u); }); diff --git a/host/hle/hle_media.cpp b/host/hle/hle_media.cpp index f1f4740..592f81b 100644 --- a/host/hle/hle_media.cpp +++ b/host/hle/hle_media.cpp @@ -662,6 +662,29 @@ void register_vblank_waits(HleRegistrar &hle) { ctx.fpr[0] = 60000.0f / 1001.0f; kernel().finish(ctx, 0u); }); + // sceDisplayGetFrameBuf(void **top, int *width, int *format, sync): what + // sceDisplaySetFrameBuf was last given. + hle.add("sceDisplay", "sceDisplayGetFrameBuf", [](Runtime &rt, AllegrexContext &ctx) { + auto &memory = rt.memory(); + if (arg(ctx, 0) != 0u) memory.store32(arg(ctx, 0), media().display.framebuffer); + if (arg(ctx, 1) != 0u) memory.store32(arg(ctx, 1), media().display.buffer_width); + if (arg(ctx, 2) != 0u) memory.store32(arg(ctx, 2), media().display.pixel_format); + kernel().finish(ctx, 0u); + }); + // The display's position in its frame, from emulated time since the last + // vblank: 286 lines a frame, the last 14 of them the vertical blank. + // Chinatown Wars and Vice City Stories import these; no game has been + // seen calling them yet. + hle.add("sceDisplay", "sceDisplayGetCurrentHcount", [](Runtime &, AllegrexContext &ctx) { + log_once("display-hcount", "[display] sceDisplayGetCurrentHcount (UNVERIFIED: no game traced yet)"); + const std::uint64_t since = kernel().now_us() - kernel().last_vblank_us(); + kernel().finish(ctx, static_cast(since * 286u / 16683u % 286u)); + }); + hle.add("sceDisplay", "sceDisplayIsVblank", [](Runtime &, AllegrexContext &ctx) { + log_once("display-isvblank", "[display] sceDisplayIsVblank (UNVERIFIED: no game traced yet)"); + const std::uint64_t since = kernel().now_us() - kernel().last_vblank_us(); + kernel().finish(ctx, since * 286u / 16683u % 286u >= 272u ? 1u : 0u); + }); hle.add("sceDisplay", "sceDisplayGetVcount", [](Runtime &, AllegrexContext &ctx) { trace_pacing("sceDisplayGetVcount", static_cast(kernel().vblank_count())); kernel().finish(ctx, static_cast(kernel().vblank_count())); @@ -907,6 +930,16 @@ void register_audio(HleRegistrar &hle) { media().output2_channel = channel; kernel().finish(ctx, 0u); }); + // sceAudioOutput2ChangeLength(samples): the output's block size. + hle.add("sceAudio", "sceAudioOutput2ChangeLength", [](Runtime &, AllegrexContext &ctx) { + if (media().output2_channel < 0) { + kernel().finish(ctx, 0x80260002u); + return; + } + log_once("audio-output2-length", "[audio] sceAudioOutput2ChangeLength (UNVERIFIED: no game traced yet)"); + media().audio[static_cast(media().output2_channel)].samples = arg(ctx, 0); + kernel().finish(ctx, 0u); + }); hle.add("sceAudio", "sceAudioOutput2Release", [](Runtime &, AllegrexContext &ctx) { if (media().output2_channel >= 0) { media().audio[static_cast(media().output2_channel)].reserved = false; diff --git a/host/hle/hle_system.cpp b/host/hle/hle_system.cpp index a382834..7f2d097 100644 --- a/host/hle/hle_system.cpp +++ b/host/hle/hle_system.cpp @@ -230,6 +230,19 @@ void register_platform(HleRegistrar &hle) { kernel().finish(ctx, 0u); }); register_rtc_calendar(hle); + // The power switch is never used here, so holding it off changes nothing. + hle.add("sceSuspendForUser", "sceKernelPowerLock", success); + hle.add("sceSuspendForUser", "sceKernelPowerUnlock", success); + // A disc game's licensee key and a PSN title's data key: nothing here + // decrypts with them yet. Chinatown Wars imports both. + hle.add("scePspNpDrm_user", "sceNpDrmSetLicenseeKey", [](Runtime &, AllegrexContext &ctx) { + log_once("npdrm-licensee", "[npdrm] sceNpDrmSetLicenseeKey (UNVERIFIED: no game traced yet)"); + kernel().finish(ctx, 0u); + }); + hle.add("scePspNpDrm_user", "sceNpDrmEdataSetupKey", [](Runtime &, AllegrexContext &ctx) { + log_once("npdrm-edata", "[npdrm] sceNpDrmEdataSetupKey (UNVERIFIED: no game traced yet)"); + kernel().finish(ctx, 0u); + }); hle.add("sceImpose", "sceImposeSetLanguageMode", success); // (int *language, int *button): the console's, as GetSystemParamInt says. hle.add("sceImpose", "sceImposeGetLanguageMode", [](Runtime &rt, AllegrexContext &ctx) { @@ -248,6 +261,18 @@ void register_platform(HleRegistrar &hle) { for (std::uint32_t i = 0; i < 16u; ++i) rt.memory().store8(arg(ctx, 0) + i, static_cast(0x10u + i)); kernel().finish(ctx, 0u); }); + // sceUtilityGetSystemParamString(id, char *buffer, int length): id 1 is + // the console's nickname. Vice City Stories and Chinatown Wars import it. + hle.add("sceUtility", "sceUtilityGetSystemParamString", [](Runtime &rt, AllegrexContext &ctx) { + constexpr std::uint32_t kInvalidId = 0x80110103u; // not traced + log_once("utility-param-string", "[utility] sceUtilityGetSystemParamString (UNVERIFIED: no game traced yet)"); + if (arg(ctx, 0) != 1u || arg(ctx, 1) == 0u || arg(ctx, 2) == 0u) { + kernel().finish(ctx, kInvalidId); + return; + } + write_cstring(rt.memory(), arg(ctx, 1), "PortableKit", arg(ctx, 2)); + kernel().finish(ctx, 0u); + }); hle.add("sceUtility", "sceUtilityGetSystemParamInt", [](Runtime &rt, AllegrexContext &ctx) { std::uint32_t value = 0u; switch (arg(ctx, 0)) { diff --git a/host/hle/hle_threadman.cpp b/host/hle/hle_threadman.cpp index 5b163b9..ea1d1fe 100644 --- a/host/hle/hle_threadman.cpp +++ b/host/hle/hle_threadman.cpp @@ -956,6 +956,26 @@ void register_kernel_library(HleRegistrar &hle) { // Bytes left on the current thread's stack, which a game checks before it // recurses. A stub returning 0 says the stack is exhausted, and the game // believes it. + // sceKernelGetThreadStackFreeSize(uid), 0 the calling thread: the space + // between the stack's bottom and where the thread's stack pointer is. + // A PSP measures the part never written (its fill pattern); this is the + // part not in use now, never less. Patapon and Chinatown Wars import it. + hle.add("ThreadManForUser", "sceKernelGetThreadStackFreeSize", [](Runtime &, AllegrexContext &ctx) { + log_once("thread-stack-free", "[kernel] sceKernelGetThreadStackFreeSize (UNVERIFIED: no game traced yet)"); + const SceUID uid = as_signed(arg(ctx, 0)); + const Thread *thread = kernel().find_thread(uid == 0 ? kernel().current_uid() : uid); + if (thread == nullptr) { + kernel().finish(ctx, error::kUnknownThid); + return; + } + const std::uint32_t sp = thread->uid == kernel().current_uid() ? ctx.gpr[29] : thread->context.gpr[29]; + kernel().finish(ctx, sp > thread->stack_bottom ? sp - thread->stack_bottom : 0u); + }); + hle.add("ThreadManForUser", "sceKernelCheckThreadStack", [](Runtime &, AllegrexContext &ctx) { + const Thread *thread = kernel().current_thread(); + const std::uint32_t sp = ctx.gpr[29]; + kernel().finish(ctx, thread != nullptr && sp > thread->stack_bottom ? sp - thread->stack_bottom : 0u); + }); hle.add("Kernel_Library", "sceKernelCheckThreadStack", [](Runtime &, AllegrexContext &ctx) { const Thread *thread = kernel().current_thread(); if (thread == nullptr) { From f2b201565167616f2c2a3afcdff72ff9b8ab1813 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:17:48 +0300 Subject: [PATCH 21/26] Add docs/HLE_COVERAGE.md: what the HLE answers and how far it is checked Stub counts per game before and after this branch, the calls verified on a game (and which), the ones written from documentation that log UNVERIFIED on first use, and the infrastructure networking answered as a PSP with no access point. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/HLE_COVERAGE.md | 77 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/HLE_COVERAGE.md diff --git a/docs/HLE_COVERAGE.md b/docs/HLE_COVERAGE.md new file mode 100644 index 0000000..34da19f --- /dev/null +++ b/docs/HLE_COVERAGE.md @@ -0,0 +1,77 @@ +# HLE coverage + +What the framework's system libraries answer, and how far each answer has been checked. The list follows the imports of five of the maintainer's discs, run on Windows with the desktop app: God of War: Chains of Olympus (`UCES-00842`), Grand Theft Auto: Chinatown Wars (`ULUS-10490`), Grand Theft Auto: Vice City Stories (`ULES-00502`), Patapon (`UCES-00995`) and Monster Hunter Portable 2nd G (`ULJM-05500`), plus Phantasy Star Portable 2 Infinity (`NPJH-50332`). + +Three kinds of answer: + +- **Verified on a game**: traced against a game calling it, with what the game does afterwards; the game named is the one it was checked with. +- **UNVERIFIED**: written from documentation (the SDK's headers and public descriptions), not yet seen called by any game. Each prints one line with `UNVERIFIED` the first time a game calls it, so the first game that does shows up in its log. +- **Not connected, by design**: infrastructure networking. It answers as a PSP with networking but no access point: libraries initialise, nothing connects. + +NIDs are added to `configs/nids.csv` only where the first word of SHA-1 of the name matches, except the three randomized ones named after public NID lists, which say so below. + +## Unimplemented imports + +`_LIST_STUBS=1` prints them; the counts are of the main executable's imports. + +| Game | Start | Now | What is left | +| --- | ---: | ---: | --- | +| God of War | 18 | 2 | `sceKernelLoadExec`, `sceAtracSetAA3DataAndGetID` | +| Chinatown Wars | 61 | 14 | `sceNetAdhocMatching*` (9), `0x91DE343C` (SysMem), `0x71EC4271` (ThreadMan), `0x05572A5F` (IoFileMgr), `0x20628E6F`, `0x46EBB729` | +| Vice City Stories | 28 | 11 | `sceNetAdhocMatching*` (9), `sceKernelReferThreadProfiler`, `sceKernelReferGlobalProfiler` | +| MHP2G | 55 | 0 | | +| Patapon | 37 | 0 | | + +## Verified on a game + +| Call | Game | What it answers | +| --- | --- | --- | +| `sceKernelIsCpuIntrEnable` | God of War | 1 while interrupts are enabled | +| `sceIoChdir`, opening disc files with write flags | God of War | paths without a device are relative to it; the open succeeds, writes fail | +| `sceIoDread` names | God of War | as the disc spells them (the game hashes the paths it lists) | +| `sceKernelReferThreadStatus` | God of War | `SceKernelThreadInfo` as `pspthreadman.h` lays it out | +| `sceKernelReferEventFlagStatus` | God of War, Patapon | `SceKernelEventFlagInfo` | +| `sceDisplayGetFramePerSec` | God of War, Patapon | 59.94 in `$f0` | +| `sceMpegAvcDecodeFlush` | God of War | drops the decoder's state | +| `sceAtracGetSecondBufferInfo` | God of War | "second buffer not needed" (0x80630022) | +| busy clock (`Kernel::charge_busy_time`) | God of War | a thread that never waits sees time pass (off: `_NO_BUSY_CLOCK`) | +| `sceKernel*MsgPipe` | Patapon (Create, Send, TryReceive) | a byte stream; Cancel and ReferStatus UNVERIFIED | +| `scePsmf*` | Patapon (SetPsmf, GetPsmfVersion, GetNumberOf(Specific)Streams, SpecifyStream, GetCurrentStreamType, GetVideoInfo) | the PSMF header; the other calls UNVERIFIED | +| `sceMpegAvcCsc` | Patapon | a decoded picture into pixels, range in pixels | +| GE SIGNAL list flow | Patapon | 0x10 jump, 0x11 call, 0x12 return | +| ATRAC streaming through `sceAtracSetData`, looping refill | Patapon | a buffer smaller than the file streams; a loop is asked for again | +| `scePower*ClockFrequency*`, `scePower_EBD177D6` | God of War, Patapon, Chinatown Wars (Set), PSP2i (GetCpu) | the clocks set are reported back; `0xEBD177D6` is named after public NID lists, and the arguments games pass (333, 333, 166) fit | +| `sceKernelSetCompiledSdkVersion370` (`0x342061E5`) | God of War, Patapon, MHP2G | named after public NID lists; called with 0x03070010 | +| `sceKernelDcacheWritebackInvalidateRange`, `sceKernelReleaseWaitThread`, `sceRtcGetTime64_t` | PSP2i | nothing to do; RELEASE_WAIT; seconds since 1970 | +| `sceKernelPowerLock`, `sceKernelPowerUnlock` | God of War, PSP2i | 0 | +| PGD files (`sceIoIoctl 0x04100001`) | PSP2i (the call and the missing-key report) | see `docs/DESKTOP_APP.md`, "PGD data"; decryption itself not yet checked on a real file | + +## UNVERIFIED + +Written from documentation; each logs `UNVERIFIED` on its first call. + +- **ThreadManForUser**: `sceKernelCancelMsgPipe`, `sceKernelReferMsgPipeStatus`, `sceKernelGetThreadStackFreeSize` (the space below the stack pointer, not the never-written part a PSP measures), `sceKernelCheckThreadStack`. +- **IoFileMgrForUser**: `sceIoMkdir`, `sceIoRmdir`, `sceIoRemove` (memory stick only). +- **sceRtc**: `GetTick`, `SetTick`, `CompareTick`, `GetCurrentClock`, `GetDayOfWeek`, `GetDaysInMonth`, `IsLeapYear`, `CheckValid`, `GetTickResolution`, `TickAdd{Ticks,Microseconds,Seconds,Minutes,Hours,Days,Weeks}`; the calendar has unit tests (`portablekit_rtc_tests`). What an invalid date returns is not traced. +- **scePsmf**: `VerifyPsmf`, `SpecifyStreamWithStreamType`, `SpecifyStreamWithStreamTypeNumber`, `GetCurrentStreamNumber`, `GetAudioInfo`, `GetHeaderSize`, `GetStreamSize`, `GetPresentationStartTime`, `GetPresentationEndTime`; error codes are the library's documented ones. +- **sceDisplay**: `GetCurrentHcount`, `IsVblank` (from emulated time, 286 lines a frame); `GetFrameBuf`. +- **sceAudio**: `Output2ChangeLength`. **sceAtrac3plus**: `GetInternalErrorInfo`. +- **sceNetAdhocctl**: `Create`, `Connect`, `Join`, `GetNameByAddr`; **sceNet**: `EtherNtostr`; **sceNetAdhocDiscover**: `RequestSuspend`. +- **sceUtility**: `GetSystemParamString` (nickname). +- **scePspNpDrm_user**: `SetLicenseeKey`, `EdataSetupKey` (accepted; nothing decrypts with them yet). +- **sceUmdUser**: `Deactivate`. + +## Not connected, by design + +`hle_net_offline.cpp`. Every call logs `UNVERIFIED` on first use. + +| Library | Answer | +| --- | --- | +| sceNetInet | Init/Term 0; sockets can be made; `connect` fails with ENETUNREACH, `send`/`recv` with ENOTCONN, `accept`/`recvfrom` with EWOULDBLOCK; `select` finds nothing ready; `InetAddr`/`InetNtop` convert; `GetErrno` reports the last errno | +| sceNetApctl | Init/Term/handlers 0; state disconnected; `GetInfo` and `Connect` fail | +| sceNetResolver | Init/Term/Create/Delete 0; `StartNtoA` fails | +| sceHttp, sceHttps | templates, connections and requests can be made; `SendRequest`, `ReadData` and the response queries fail | +| sceSsl | Init/End 0 | +| sceUtility | Load/UnloadNetModule 0; the browser dialog (`HtmlViewer`) opens and closes at once | + +Ad hoc play is `hle_adhoc.cpp` (Yakumo's, generalised); `sceNetAdhocMatching` is not implemented yet. From 3f0b2233675f1b53cfdffb852c5d0990b04176b3 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:19:24 +0300 Subject: [PATCH 22/26] Document TRACE_GE_LIST, TRACE_SAS and NO_BUSY_CLOCK Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/BRINGING_UP_A_GAME.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/BRINGING_UP_A_GAME.md b/docs/BRINGING_UP_A_GAME.md index 8ffd641..2b64b6f 100644 --- a/docs/BRINGING_UP_A_GAME.md +++ b/docs/BRINGING_UP_A_GAME.md @@ -80,6 +80,9 @@ Every one of these takes the profile's own prefix, so `TENKAWA_TRACE_KERNEL` for | `_STRICT_HLE` | Do not bind logging stubs for unimplemented imports, so the game stops at the first one instead of carrying on with a wrong answer | | `_TRACE_KERNEL`, `_TRACE_IO`, `_TRACE_GE`, `_TRACE_SAVEDATA`, … | One subsystem each | | `_TRACE_SYNC` | Every kernel object a thread waits on, which is how a deadlock is read | +| `_TRACE_GE_LIST=N[:M]` | Every command word of display-list runs N to N+M-1: what a screen that draws nothing is built from | +| `_TRACE_SAS` | SAS voices set, keyed on and off, and each change of the end flags a game polls | +| `_NO_BUSY_CLOCK` | Keep emulated time still for a thread that never waits, as before the busy clock (`Kernel::charge_busy_time`) | | `PSPRECOMP_NO_INTERPRETER=1` | Turn the interpreter off. Nothing runs without a corpus; with one, the runtime stops at the first address the recompiler missed and names it | | `PSPRECOMP_MAX_DISPATCHES` | Stop after this many dispatches, for a bounded run | | `PSPRECOMP_INTERPRETER_WATCH` | Guest addresses, comma separated: report the registers each time interpreted code reaches one. `PSPRECOMP_INTERPRETER_WATCH_WORDS=N` also dumps N words at `a0`, and `PSPRECOMP_INTERPRETER_WATCH_LIMIT` bounds how many times each address is reported (40 by default, 0 for no limit) | From 8c14a91f07ff13ebeed24c2a79449ea44304d3a8 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:24:19 +0300 Subject: [PATCH 23/26] Trace where sceMpegAvcDecode writes its picture, and in which format Co-Authored-By: Claude Opus 5.5 (1M context) --- host/hle/hle_mpeg.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/host/hle/hle_mpeg.cpp b/host/hle/hle_mpeg.cpp index 22666ef..08fb00c 100644 --- a/host/hle/hle_mpeg.cpp +++ b/host/hle/hle_mpeg.cpp @@ -525,7 +525,10 @@ void register_decoding(HleRegistrar &hle) { } if (arg(ctx, 4) != 0u) memory.store32(arg(ctx, 4), produced ? 1u : 0u); finish_traced(ctx, "sceMpegAvcDecode", 0u, - produced ? "picture " + std::to_string(state->pictures) + " width " + std::to_string(width) : "none", 5u); + produced ? "picture " + std::to_string(state->pictures) + " width " + std::to_string(width) + " mode " + + std::to_string(state->pixel_mode) + " to " + psprecomp::hex32(memory.load32(arg(ctx, 3))) + : "none", + 5u); }); // sceMpegAvcDecodeStop(mpeg, frameWidth, bufferPointer, outStatus): a // picture the decoder still holds at the end of the stream. From 6b78a9c1b4b305726ca110b853072bfe4f08741b Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:40:26 +0300 Subject: [PATCH 24/26] Show movie pictures the game decodes straight into VRAM God of War (UCES00842) decodes its intro with sceMpegAvcDecode into its two display buffers in VRAM (0x04000000, 0x0408C000, 32-bit) and draws the subtitles over them with the GE. The renderer keeps each framebuffer on the GPU and never looked at what the CPU wrote there: the intro showed only the subtitles on black. A picture decoded into VRAM now goes to that framebuffer's GPU target (VulkanRenderer::upload_frame), and the GE's draws land on top. 16-bit formats are logged and not shown. _NO_VRAM_UPLOAD turns it off. Co-Authored-By: Claude Opus 5.5 (1M context) --- host/hle/hle_common.hpp | 4 ++++ host/hle/hle_media.cpp | 21 +++++++++++++++++++++ host/hle/hle_mpeg.cpp | 5 ++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/host/hle/hle_common.hpp b/host/hle/hle_common.hpp index 21638a3..8233069 100644 --- a/host/hle/hle_common.hpp +++ b/host/hle/hle_common.hpp @@ -84,6 +84,10 @@ void register_atrac(HleRegistrar &hle); void register_mpeg(HleRegistrar &hle); void register_psmfplayer(HleRegistrar &hle); void register_psmf(HleRegistrar &hle); +// A picture the CPU wrote into a VRAM framebuffer, handed to the renderer +// (hle_media.cpp). +void show_cpu_picture_in_vram(Runtime &rt, std::uint32_t address, std::uint32_t width, std::uint32_t height, + std::uint32_t stride, std::uint32_t pixel_mode); void register_net_offline(HleRegistrar &hle); void register_font(HleRegistrar &hle); void register_utility(HleRegistrar &hle, const std::filesystem::path &memory_stick); diff --git a/host/hle/hle_media.cpp b/host/hle/hle_media.cpp index 592f81b..3b3727d 100644 --- a/host/hle/hle_media.cpp +++ b/host/hle/hle_media.cpp @@ -1098,6 +1098,27 @@ void initialize_renderer() { #endif } +// A picture the CPU wrote straight into a VRAM framebuffer (the movie +// player decoding into the display buffers, as God of War's does): the +// renderer keeps each framebuffer on the GPU, so the pixels go to that +// framebuffer's target, and what the GE draws afterwards (subtitles) lands +// on top. 32-bit pixels only. _NO_VRAM_UPLOAD turns it off. +void show_cpu_picture_in_vram(Runtime &rt, std::uint32_t address, std::uint32_t width, std::uint32_t height, + std::uint32_t stride, std::uint32_t pixel_mode) { +#if defined(PORTABLEKIT_HAS_RENDERER) + static const bool off = portablekit::env("NO_VRAM_UPLOAD") != nullptr; + if (off || (address & 0x1F000000u) != kEdramBase || !media().renderer || !media().renderer->available()) return; + if (pixel_mode != 3u) { + log_once("vram-upload-format", "[mpeg] a picture decoded into VRAM in a 16-bit format is not shown"); + return; + } + const std::size_t bytes = static_cast(stride) * height * 4u; + media().renderer->upload_frame(address | kEdramBase, rt.memory().raw_pointer(address, bytes), width, height, stride); +#else + (void)rt, (void)address, (void)width, (void)height, (void)stride, (void)pixel_mode; +#endif +} + void register_media(HleRegistrar &hle) { initialize_renderer(); register_display_ctrl(hle); diff --git a/host/hle/hle_mpeg.cpp b/host/hle/hle_mpeg.cpp index 08fb00c..b4813f5 100644 --- a/host/hle/hle_mpeg.cpp +++ b/host/hle/hle_mpeg.cpp @@ -521,7 +521,10 @@ void register_decoding(HleRegistrar &hle) { if (width == 0u) width = state->default_frame_width != 0u ? state->default_frame_width : state->picture.width; if (produced) { ++state->pictures; - store_pixels(memory, memory.load32(arg(ctx, 3)), width, state->pixel_mode, state->picture); + const std::uint32_t destination = memory.load32(arg(ctx, 3)); + store_pixels(memory, destination, width, state->pixel_mode, state->picture); + show_cpu_picture_in_vram(rt, destination, state->picture.width, state->picture.height, width, + state->pixel_mode); } if (arg(ctx, 4) != 0u) memory.store32(arg(ctx, 4), produced ? 1u : 0u); finish_traced(ctx, "sceMpegAvcDecode", 0u, From 167108d43ce6d493ebd8440eb3d78acd2a7750f6 Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 23:07:37 +0300 Subject: [PATCH 25/26] Answer the profiler queries as a PSP with the profiler off sceKernelReferThreadProfiler and sceKernelReferGlobalProfiler fill the caller's PspDebugProfilerRegs (20 words, per pspsdk's pspdebug.h) with zeros and return 0. Vice City Stories and PSP2i import them; neither has been traced calling them yet, so they log UNVERIFIED on first use. Co-Authored-By: Claude Opus 5.5 (1M context) --- configs/nids.csv | 2 ++ docs/HLE_COVERAGE.md | 4 ++-- host/hle/hle_threadman.cpp | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/configs/nids.csv b/configs/nids.csv index e55904d..5c32ea3 100644 --- a/configs/nids.csv +++ b/configs/nids.csv @@ -514,6 +514,8 @@ ThreadManForUser,0x33BE4024,sceKernelReferMsgPipeStatus ThreadManForUser,0x17C1684E,sceKernelReferThreadStatus ThreadManForUser,0xA66B0120,sceKernelReferEventFlagStatus ThreadManForUser,0x2C34E053,sceKernelReleaseWaitThread +ThreadManForUser,0x64D4540E,sceKernelReferThreadProfiler +ThreadManForUser,0x8218B4DD,sceKernelReferGlobalProfiler ThreadManForUser,0xD13BDE95,sceKernelCheckThreadStack UtilsForUser,0x27CC57F0,sceKernelLibcTime UtilsForUser,0x3EE30821,sceKernelDcacheWritebackRange diff --git a/docs/HLE_COVERAGE.md b/docs/HLE_COVERAGE.md index 34da19f..a29b91f 100644 --- a/docs/HLE_COVERAGE.md +++ b/docs/HLE_COVERAGE.md @@ -18,7 +18,7 @@ NIDs are added to `configs/nids.csv` only where the first word of SHA-1 of the n | --- | ---: | ---: | --- | | God of War | 18 | 2 | `sceKernelLoadExec`, `sceAtracSetAA3DataAndGetID` | | Chinatown Wars | 61 | 14 | `sceNetAdhocMatching*` (9), `0x91DE343C` (SysMem), `0x71EC4271` (ThreadMan), `0x05572A5F` (IoFileMgr), `0x20628E6F`, `0x46EBB729` | -| Vice City Stories | 28 | 11 | `sceNetAdhocMatching*` (9), `sceKernelReferThreadProfiler`, `sceKernelReferGlobalProfiler` | +| Vice City Stories | 28 | 9 | `sceNetAdhocMatching*` (9) | | MHP2G | 55 | 0 | | | Patapon | 37 | 0 | | @@ -50,7 +50,7 @@ NIDs are added to `configs/nids.csv` only where the first word of SHA-1 of the n Written from documentation; each logs `UNVERIFIED` on its first call. -- **ThreadManForUser**: `sceKernelCancelMsgPipe`, `sceKernelReferMsgPipeStatus`, `sceKernelGetThreadStackFreeSize` (the space below the stack pointer, not the never-written part a PSP measures), `sceKernelCheckThreadStack`. +- **ThreadManForUser**: `sceKernelReferThreadProfiler`, `sceKernelReferGlobalProfiler` (the profiler reads as off, every counter 0), `sceKernelCancelMsgPipe`, `sceKernelReferMsgPipeStatus`, `sceKernelGetThreadStackFreeSize` (the space below the stack pointer, not the never-written part a PSP measures), `sceKernelCheckThreadStack`. - **IoFileMgrForUser**: `sceIoMkdir`, `sceIoRmdir`, `sceIoRemove` (memory stick only). - **sceRtc**: `GetTick`, `SetTick`, `CompareTick`, `GetCurrentClock`, `GetDayOfWeek`, `GetDaysInMonth`, `IsLeapYear`, `CheckValid`, `GetTickResolution`, `TickAdd{Ticks,Microseconds,Seconds,Minutes,Hours,Days,Weeks}`; the calendar has unit tests (`portablekit_rtc_tests`). What an invalid date returns is not traced. - **scePsmf**: `VerifyPsmf`, `SpecifyStreamWithStreamType`, `SpecifyStreamWithStreamTypeNumber`, `GetCurrentStreamNumber`, `GetAudioInfo`, `GetHeaderSize`, `GetStreamSize`, `GetPresentationStartTime`, `GetPresentationEndTime`; error codes are the library's documented ones. diff --git a/host/hle/hle_threadman.cpp b/host/hle/hle_threadman.cpp index ea1d1fe..f4ec4b2 100644 --- a/host/hle/hle_threadman.cpp +++ b/host/hle/hle_threadman.cpp @@ -971,6 +971,20 @@ void register_kernel_library(HleRegistrar &hle) { const std::uint32_t sp = thread->uid == kernel().current_uid() ? ctx.gpr[29] : thread->context.gpr[29]; kernel().finish(ctx, sp > thread->stack_bottom ? sp - thread->stack_bottom : 0u); }); + // sceKernelReferThreadProfiler(regs) and sceKernelReferGlobalProfiler(regs): + // the hardware profiler's counters, PspDebugProfilerRegs in pspsdk's + // pspdebug.h (20 words, `enable` first). A retail PSP has the profiler + // off; here it reads as off with every counter 0. Vice City Stories and + // PSP2i import them. + for (const char *name : {"sceKernelReferThreadProfiler", "sceKernelReferGlobalProfiler"}) { + hle.add("ThreadManForUser", name, [name](Runtime &rt, AllegrexContext &ctx) { + log_once(std::string("profiler-") + name, std::string("[kernel] ") + name + " (UNVERIFIED: no game traced yet)"); + constexpr std::uint32_t kProfilerRegsBytes = 20u * 4u; + if (const std::uint32_t regs = arg(ctx, 0); regs != 0u) + for (std::uint32_t at = 0; at < kProfilerRegsBytes; at += 4u) rt.memory().store32(regs + at, 0u); + kernel().finish(ctx, 0u); + }); + } hle.add("ThreadManForUser", "sceKernelCheckThreadStack", [](Runtime &, AllegrexContext &ctx) { const Thread *thread = kernel().current_thread(); const std::uint32_t sp = ctx.gpr[29]; From a19f4dd010c7130b491d60bc7c381fc8b5ff0cfa Mon Sep 17 00:00:00 2001 From: MHunterG <48920434+MHunterG@users.noreply.github.com> Date: Sat, 26 Sep 2026 23:17:58 +0300 Subject: [PATCH 26/26] Close the response file before the test removes it Windows does not remove a file that is still open: fs::remove threw, and the uncaught exception ended the test with 0xC0000409 on MSVC. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/response_file_tests.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/response_file_tests.cpp b/tests/response_file_tests.cpp index f769bc7..2e09420 100644 --- a/tests/response_file_tests.cpp +++ b/tests/response_file_tests.cpp @@ -66,9 +66,12 @@ int main() { "the program stays, everything else becomes @file"); check(command_line_length(short_command) < 1000u, "and the command line is short"); - std::ifstream in(file, std::ios::binary); std::stringstream text; - text << in.rdbuf(); + { + // Closed before the file is removed: Windows does not remove an open file. + std::ifstream in(file, std::ios::binary); + text << in.rdbuf(); + } const auto read = read_gnu(text.str()); check(read.size() == command.size() - 1u, "the file holds every other argument"); bool same = read.size() == command.size() - 1u; @@ -81,7 +84,8 @@ int main() { check(same, "and reads back as the same arguments, with forward slashes"); check(text.str().find('\\') == std::string::npos || text.str().find("\\\"") != std::string::npos, "no backslash is left but the quote escapes"); - fs::remove(file); + std::error_code ignored; + fs::remove(file, ignored); const auto unchanged = with_response_file({"clang++"}, 1u, file); check(unchanged.size() == 1u && !fs::exists(file), "a command with nothing to move is left alone");