// LCS (GTA: Liberty City Stories) kernel HLE. #include "lcs_profile.hpp" #include "display_window.hpp" #include "lcs_ge_exec.hpp" #include "lcs_sas.hpp" #include "ge_gpu_backend.hpp" #include "lcs_media_decoder.hpp" #include "lcs_audio_output.hpp" #include "lcs_fps_overlay.hpp" #include "psprecomp/common.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace lcs { namespace { enum class ThreadState { Created, Ready, Running, Sleeping, Delayed, Completed, IoDeferred, }; struct ThreadRecord { std::string name; std::uint32_t entry{}; std::uint32_t priority{}; std::uint32_t stack_size{}; std::uint32_t attributes{}; std::uint32_t stack_top{}; std::uint32_t stack_bottom{}; std::uint32_t kernel_context{}; ThreadState state{ThreadState::Created}; std::uint32_t exit_status{}; bool externally_suspended{}; psprecomp::AllegrexContext suspended_context{}; std::uint32_t wakeup_count{}; std::uint64_t delay_until_us{}; std::uint64_t delay_sequence{}; }; struct ThreadContinuation { std::int32_t uid{}; psprecomp::AllegrexContext context{}; std::uint64_t ready_sequence{}; }; struct FreeThreadStack { std::uint32_t bottom{}; std::uint32_t top{}; }; struct ThreadTable { std::int32_t next_uid{1}; std::int32_t current_uid{0}; std::uint32_t next_stack_top{0x0A000000u}; std::uint64_t next_ready_sequence{1u}; std::uint64_t next_delay_sequence{1u}; std::unordered_map threads; std::vector continuations; std::unordered_map> thread_end_waiters; std::vector free_stacks; }; struct PartitionBlock { std::string name; std::uint32_t address{}; std::uint32_t size{}; }; struct PartitionTable { std::int32_t next_uid{0x100}; std::uint32_t next_address{}; std::unordered_map blocks; }; struct CallbackRecord { std::string name; std::uint32_t function{}; std::uint32_t common{}; std::int32_t owner_uid{}; std::uint32_t notify_count{}; std::uint32_t notify_argument{}; }; struct CallbackTable { std::int32_t next_uid{0x200}; std::unordered_map callbacks; }; struct SemaphoreWaiter { std::int32_t uid{}; psprecomp::AllegrexContext context{}; std::int32_t requested{}; }; struct SemaphoreRecord { std::string name; std::int32_t count{}; std::int32_t maximum{}; std::vector waiters; }; struct SemaphoreTable { std::int32_t next_uid{0x300}; std::unordered_map semaphores; }; struct EventFlagWaiter { std::int32_t uid{}; psprecomp::AllegrexContext context{}; std::uint32_t requested{}; std::uint32_t mode{}; std::uint32_t output_address{}; }; struct EventFlagRecord { std::string name; std::uint32_t attributes{}; std::uint32_t initial_pattern{}; std::uint32_t current_pattern{}; std::vector waiters; }; struct EventFlagTable { std::int32_t next_uid{0x600}; std::unordered_map flags; }; struct FixedPoolRecord { std::string name; std::uint32_t address{}; std::uint32_t block_size{}; std::uint32_t block_count{}; std::vector allocated; }; struct FixedPoolTable { std::int32_t next_uid{0x500}; std::unordered_map pools; }; enum class AsyncReturnKind { UserCallback, MpegRingbuffer, GeFinishThenDelay, SubInterrupt, }; struct AsyncReturnFrame { psprecomp::AllegrexContext resume{}; std::int32_t callback_uid{}; AsyncReturnKind kind{AsyncReturnKind::UserCallback}; std::uint32_t ring{}; std::uint32_t delay_us{}; bool vblank_wait{}; }; struct PendingGeCallback { std::int32_t callback_uid{}; std::uint32_t function{}; std::uint32_t finish_argument{}; std::uint32_t user_argument{}; }; struct DirectoryHandle { std::vector entries; std::size_t index{}; }; struct RawSectorFile { std::uint64_t base{}; std::uint64_t size{}; }; struct VirtualDiscFile { std::filesystem::path native_path; std::uint32_t start_sector{}; std::uint64_t size{}; }; struct VirtualDiscHandle { std::uint64_t base_offset{}; std::uint64_t length{}; std::uint64_t position{}; }; struct FileTable { std::int32_t next_fd{3}; std::unordered_map files; std::unordered_map directories; std::unordered_map raw_sector_files; std::uint32_t next_virtual_sector{32u}; std::unordered_map virtual_files_by_path; std::map virtual_path_by_sector; std::unordered_map virtual_disc_handles; }; ThreadTable thread_table; PartitionTable partition_table; CallbackTable callback_table; SemaphoreTable semaphore_table; EventFlagTable event_flag_table; FixedPoolTable fixed_pool_table; FileTable file_table; const VirtualDiscFile *register_virtual_disc_file(const std::filesystem::path &path) { std::error_code error; if (!std::filesystem::is_regular_file(path, error) || error) return nullptr; std::filesystem::path normalized = std::filesystem::weakly_canonical(path, error); if (error) normalized = path.lexically_normal(); const std::string key = normalized.generic_string(); if (const auto found = file_table.virtual_files_by_path.find(key); found != file_table.virtual_files_by_path.end()) { return &found->second; } const std::uint64_t size = std::filesystem::file_size(path, error); if (error) return nullptr; const std::uint64_t sector_count = std::max(1u, (size + 2047u) / 2048u); if (static_cast(file_table.next_virtual_sector) + sector_count > 0xFFFFFFFFull) { return nullptr; } VirtualDiscFile item{}; item.native_path = path; item.start_sector = file_table.next_virtual_sector; item.size = size; file_table.next_virtual_sector += static_cast(sector_count); const auto [inserted, ok] = file_table.virtual_files_by_path.emplace(key, std::move(item)); if (!ok) return &inserted->second; file_table.virtual_path_by_sector.emplace(inserted->second.start_sector, key); return &inserted->second; } struct DiscReadStats { std::uint64_t bytes_from_files{}; std::uint64_t bytes_zero_filled{}; std::uint64_t zero_fill_events{}; std::uint64_t short_reads{}; }; DiscReadStats disc_read_stats; std::size_t read_virtual_disc(VirtualDiscHandle &handle, std::span output) { if (handle.position >= handle.length || output.empty()) return 0u; const std::uint64_t available = handle.length - handle.position; const std::size_t requested = static_cast(std::min(available, output.size())); std::fill(output.begin(), output.begin() + requested, 0u); std::size_t written = 0u; while (written < requested) { const std::uint64_t absolute = handle.base_offset + handle.position + written; const std::uint64_t sector64 = absolute / 2048u; if (sector64 > 0xFFFFFFFFull) break; const auto next = file_table.virtual_path_by_sector.upper_bound(static_cast(sector64)); const VirtualDiscFile *file = nullptr; if (next != file_table.virtual_path_by_sector.begin()) { const auto previous = std::prev(next); const auto found = file_table.virtual_files_by_path.find(previous->second); if (found != file_table.virtual_files_by_path.end()) { const std::uint64_t file_start = static_cast(found->second.start_sector) * 2048u; if (absolute >= file_start && absolute < file_start + found->second.size) { file = &found->second; } } } if (file != nullptr && std::getenv("LCS_SKIP_MOVIES") != nullptr && file->native_path.extension() == ".PMF" && file->native_path.parent_path().filename() == "MOVIES") { static bool reported = false; if (!reported) { reported = true; std::cerr << "[io] suppressing movie sectors for \"" << file->native_path.filename().string() << "\"\n"; } break; } if (file != nullptr) { const std::uint64_t file_start = static_cast(file->start_sector) * 2048u; const std::uint64_t file_offset = absolute - file_start; const std::size_t chunk = static_cast( std::min(requested - written, file->size - file_offset)); std::ifstream input(file->native_path, std::ios::binary); if (!input) break; input.seekg(static_cast(file_offset), std::ios::beg); input.read(reinterpret_cast(output.data() + written), static_cast(chunk)); const auto actual = static_cast(input.gcount()); written += actual; disc_read_stats.bytes_from_files += actual; if (actual != chunk) { ++disc_read_stats.short_reads; break; } continue; } std::uint64_t zero_end = handle.base_offset + handle.length; if (next != file_table.virtual_path_by_sector.end()) zero_end = std::min(zero_end, static_cast(next->first) * 2048u); if (zero_end <= absolute) zero_end = absolute + 1u; const std::size_t filled = static_cast( std::min(requested - written, zero_end - absolute)); written += filled; disc_read_stats.bytes_zero_filled += filled; ++disc_read_stats.zero_fill_events; } handle.position += written; return written; } const VirtualDiscFile *find_virtual_disc_file(std::uint32_t start_sector) { const auto sector = file_table.virtual_path_by_sector.find(start_sector); if (sector == file_table.virtual_path_by_sector.end()) return nullptr; const auto file = file_table.virtual_files_by_path.find(sector->second); if (file == file_table.virtual_files_by_path.end()) return nullptr; return &file->second; } std::unordered_map loaded_modules; std::int32_t next_module_uid{0x400}; bool volatile_memory_locked{}; bool umd_activated{}; bool umd_callback_notified{}; void notify_umd_callback() { if (!umd_activated || umd_callback_notified) return; for (auto &[id, callback] : callback_table.callbacks) { if (callback.name != "UMDCallback") continue; callback.notify_count = 1u; callback.notify_argument = 0x6u; // PSP_UMD_PRESENT | PSP_UMD_READY umd_callback_notified = true; } } std::uint32_t general_purpose_io{}; std::uint32_t memory_stick_fat_state = 1u; struct AudioChannelState { bool reserved{}; std::uint32_t sample_count{}; std::uint32_t format{}; std::uint64_t busy_until_us{}; std::uint64_t queue_anchor_us{}; std::uint64_t queued_frames{}; bool queue_active{}; }; std::array audio_channels{}; std::uint32_t mpeg_read_thread_args = 0u; struct MpegStreamState { std::uint32_t type{}; std::uint32_t number{}; }; struct ParsedPsmfHeader { std::uint32_t raw_version{}; std::uint32_t stream_offset{}; std::uint32_t stream_size{}; std::uint64_t first_timestamp{}; std::uint64_t last_timestamp{}; std::uint32_t width{}; std::uint32_t height{}; }; struct MpegContextState { std::uint32_t ring_address{}; std::unordered_map streams; std::array avc_es_buffers{}; std::uint32_t video_au_count{}; std::uint32_t audio_au_count{}; ParsedPsmfHeader header{}; bool analyzed{}; std::filesystem::path source_path; VideoStreamDecoder video; bool video_eof{}; PmfAudioDecoder audio; std::filesystem::path audio_source; }; std::uint32_t read_be32(std::span bytes, std::size_t offset) { return (static_cast(bytes[offset]) << 24u) | (static_cast(bytes[offset + 1u]) << 16u) | (static_cast(bytes[offset + 2u]) << 8u) | static_cast(bytes[offset + 3u]); } std::uint64_t read_psmf_timestamp(std::span bytes, std::size_t offset) { return static_cast(bytes[offset + 5u]) | (static_cast(bytes[offset + 4u]) << 8u) | (static_cast(bytes[offset + 3u]) << 16u) | (static_cast(bytes[offset + 2u]) << 24u) | (static_cast(bytes[offset + 1u]) << 32u) | (static_cast(bytes[offset]) << 36u); } bool parse_psmf_header(std::span bytes, ParsedPsmfHeader &header) { if (bytes.size() < 2048u || bytes[0] != 'P' || bytes[1] != 'S' || bytes[2] != 'M' || bytes[3] != 'F') return false; header.raw_version = static_cast(bytes[4]) | (static_cast(bytes[5]) << 8u) | (static_cast(bytes[6]) << 16u) | (static_cast(bytes[7]) << 24u); const bool known_version = header.raw_version == 0x32313030u || header.raw_version == 0x33313030u || header.raw_version == 0x34313030u || header.raw_version == 0x35313030u; if (!known_version) return false; header.stream_offset = read_be32(bytes, 8u); header.stream_size = read_be32(bytes, 12u); header.first_timestamp = read_psmf_timestamp(bytes, 0x54u); header.last_timestamp = read_psmf_timestamp(bytes, 0x5Au); header.width = static_cast(bytes[142u]) * 16u; header.height = static_cast(bytes[143u]) * 16u; return true; } void write_mpeg_timestamp(psprecomp::GuestMemory &memory, std::uint32_t address, std::uint64_t value) { memory.store32(address, static_cast(value >> 32u)); memory.store32(address + 4u, static_cast(value)); } std::filesystem::path identify_pmf_source(const ParsedPsmfHeader &parsed) { const std::uint64_t expected_size = static_cast(parsed.stream_offset) + parsed.stream_size; for (const auto &[key, file] : file_table.virtual_files_by_path) { if (file.size != expected_size) continue; std::string extension = file.native_path.extension().string(); std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) { return static_cast(std::toupper(ch)); }); if (extension != ".PMF") continue; std::array candidate{}; std::ifstream input(file.native_path, std::ios::binary); if (!input) continue; input.read(reinterpret_cast(candidate.data()), static_cast(candidate.size())); ParsedPsmfHeader candidate_header{}; if (!parse_psmf_header(candidate, candidate_header)) continue; if (candidate_header.stream_offset == parsed.stream_offset && candidate_header.stream_size == parsed.stream_size && candidate_header.width == parsed.width && candidate_header.height == parsed.height) { return file.native_path; } } return {}; } std::filesystem::path find_pmf_on_disc(const std::filesystem::path &root, const ParsedPsmfHeader &parsed) { const std::uint64_t expected_size = static_cast(parsed.stream_offset) + parsed.stream_size; std::error_code error; if (root.empty() || !std::filesystem::is_directory(root, error)) return {}; for (std::filesystem::recursive_directory_iterator it(root, error), end; it != end; it.increment(error)) { if (error) break; if (!it->is_regular_file(error)) continue; std::string extension = it->path().extension().string(); std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) { return static_cast(std::toupper(ch)); }); if (extension != ".PMF") continue; if (std::filesystem::file_size(it->path(), error) != expected_size || error) continue; std::array candidate{}; std::ifstream input(it->path(), std::ios::binary); if (!input) continue; input.read(reinterpret_cast(candidate.data()), static_cast(candidate.size())); ParsedPsmfHeader candidate_header{}; if (!parse_psmf_header(candidate, candidate_header)) continue; if (candidate_header.stream_offset == parsed.stream_offset && candidate_header.stream_size == parsed.stream_size) { return it->path(); } } return {}; } bool open_video_decoder(MpegContextState &state) { if (state.video.is_open()) return true; if (state.source_path.empty() || state.header.width == 0u || state.header.height == 0u) return false; if (!state.video.open(state.source_path)) return false; state.video_eof = false; if (std::getenv("LCS_MPEG_DIAG") != nullptr) { std::cerr << "[mpeg] decoder opened \"" << state.source_path.string() << "\" " << state.header.width << "x" << state.header.height << "\n"; } return true; } bool read_video_frame(MpegContextState &state, std::span frame) { if (!open_video_decoder(state)) return false; if (state.video.read(frame) < frame.size()) { state.video_eof = true; return false; } return true; } std::uint32_t mpeg_au_limit(const MpegContextState &state) { if (const char *text = std::getenv("LCS_MPEG_AU_LIMIT"); text != nullptr && *text != '\0') return static_cast(std::strtoul(text, nullptr, 0)); if (state.analyzed && state.header.last_timestamp > state.header.first_timestamp) { return static_cast( (state.header.last_timestamp - state.header.first_timestamp) / 3003u) + 1u; } return 0xFFFFFFFFu; } std::unordered_map mpeg_contexts; std::uint32_t next_mpeg_stream_id{1u}; struct ParsedAtracHeader { std::uint16_t format_tag{}; std::uint16_t channels{}; std::uint32_t sample_rate{}; std::uint32_t average_bytes_per_second{}; std::uint16_t block_align{}; std::uint16_t bits_per_sample{}; std::uint32_t data_offset{}; std::uint32_t data_size{}; std::uint32_t file_size{}; std::uint32_t total_samples{}; std::int32_t loop_start{-1}; std::int32_t loop_end{-1}; bool atrac3plus{}; }; struct AtracContextState { bool allocated{}; ParsedAtracHeader header{}; std::uint32_t buffer_address{}; std::uint32_t initial_read_size{}; std::uint32_t buffer_size{}; std::uint32_t buffered_encoded_bytes{}; std::uint32_t next_file_offset{}; std::uint32_t write_offset{}; std::uint32_t last_writable_bytes{}; std::uint64_t sample_position{}; std::int32_t loop_num{}; std::uint32_t internal_error{}; std::filesystem::path source_path; AudioStreamDecoder decoder; bool decoder_eof{}; }; std::array atrac_contexts{}; bool atrac_diag_enabled() { static const bool enabled = std::getenv("LCS_ATRAC_DIAG") != nullptr || std::getenv("PSPRECOMP_ATRAC_DIAG") != nullptr; return enabled; } std::uint16_t read_le16(std::span bytes, std::size_t offset) { return static_cast(bytes[offset]) | static_cast(static_cast(bytes[offset + 1u]) << 8u); } std::uint32_t read_le32(std::span bytes, std::size_t offset) { return static_cast(bytes[offset]) | (static_cast(bytes[offset + 1u]) << 8u) | (static_cast(bytes[offset + 2u]) << 16u) | (static_cast(bytes[offset + 3u]) << 24u); } bool parse_atrac_header(std::span bytes, ParsedAtracHeader &header) { if (bytes.size() < 12u || std::memcmp(bytes.data(), "RIFF", 4u) != 0 || std::memcmp(bytes.data() + 8u, "WAVE", 4u) != 0) return false; const std::uint64_t declared_file_size = static_cast(read_le32(bytes, 4u)) + 8u; if (declared_file_size > 0xFFFFFFFFull) return false; header = ParsedAtracHeader{}; header.file_size = static_cast(declared_file_size); bool have_fmt = false; bool have_data = false; for (std::size_t offset = 12u; offset + 8u <= bytes.size();) { const std::uint32_t chunk_size = read_le32(bytes, offset + 4u); const std::size_t payload = offset + 8u; const std::uint64_t next64 = static_cast(payload) + chunk_size + (chunk_size & 1u); if (next64 > bytes.size()) { if (std::memcmp(bytes.data() + offset, "data", 4u) == 0) { header.data_offset = static_cast(payload); header.data_size = chunk_size; have_data = true; } break; } if (std::memcmp(bytes.data() + offset, "fmt ", 4u) == 0 && chunk_size >= 16u) { header.format_tag = read_le16(bytes, payload + 0u); header.channels = read_le16(bytes, payload + 2u); header.sample_rate = read_le32(bytes, payload + 4u); header.average_bytes_per_second = read_le32(bytes, payload + 8u); header.block_align = read_le16(bytes, payload + 12u); header.bits_per_sample = read_le16(bytes, payload + 14u); have_fmt = true; } else if (std::memcmp(bytes.data() + offset, "fact", 4u) == 0 && chunk_size >= 4u) { header.total_samples = read_le32(bytes, payload); } else if (std::memcmp(bytes.data() + offset, "smpl", 4u) == 0 && chunk_size >= 60u) { const std::uint32_t loop_count = read_le32(bytes, payload + 28u); if (loop_count != 0u) { header.loop_start = static_cast(read_le32(bytes, payload + 44u)); header.loop_end = static_cast(read_le32(bytes, payload + 48u)); } } else if (std::memcmp(bytes.data() + offset, "data", 4u) == 0) { header.data_offset = static_cast(payload); header.data_size = chunk_size; have_data = true; } offset = static_cast(next64); } if (!have_fmt || !have_data || header.channels == 0u || header.channels > 2u || header.sample_rate == 0u || header.block_align == 0u) return false; header.atrac3plus = header.format_tag == 0xFFFEu; if (!header.atrac3plus && header.format_tag != 0x0270u) return false; if (header.total_samples == 0u) { const std::uint32_t samples_per_frame = header.atrac3plus ? 2048u : 1024u; header.total_samples = (header.data_size / header.block_align) * samples_per_frame; } return true; } bool is_atrac_extension(const std::filesystem::path &path) { std::string extension = path.extension().string(); std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) { return static_cast(std::toupper(ch)); }); return extension == ".AT3" || extension == ".AA3" || extension == ".OMA"; } bool atrac_file_matches(const std::filesystem::path &path, std::span header) { const std::size_t compare_size = std::min(header.size(), 256u); std::vector candidate(compare_size); std::ifstream input(path, std::ios::binary); if (!input) return false; input.read(reinterpret_cast(candidate.data()), static_cast(candidate.size())); return input.gcount() == static_cast(candidate.size()) && std::equal(candidate.begin(), candidate.end(), header.begin()); } std::filesystem::path identify_atrac_source(const std::filesystem::path &disc_root, std::span header, const ParsedAtracHeader &parsed) { for (const auto &[key, file] : file_table.virtual_files_by_path) { if (file.size != parsed.file_size || !is_atrac_extension(file.native_path)) continue; if (atrac_file_matches(file.native_path, header)) return file.native_path; } std::error_code error; if (disc_root.empty() || !std::filesystem::is_directory(disc_root, error)) return {}; for (std::filesystem::recursive_directory_iterator it(disc_root, error), end; it != end; it.increment(error)) { if (error) break; if (!it->is_regular_file(error) || !is_atrac_extension(it->path())) continue; if (std::filesystem::file_size(it->path(), error) != parsed.file_size || error) continue; if (atrac_file_matches(it->path(), header)) return it->path(); } return {}; } constexpr std::uint32_t kAtracOutputChannels = 2u; void close_atrac_decoder(AtracContextState &state) { state.decoder.close(); state.decoder_eof = false; } bool open_atrac_decoder(AtracContextState &state) { if (state.decoder.is_open()) return true; if (state.source_path.empty()) return false; std::uint64_t seek = state.sample_position; if (state.header.total_samples != 0u && seek > state.header.total_samples) seek = state.header.total_samples; if (!state.decoder.open(state.source_path, state.header.sample_rate, kAtracOutputChannels, seek)) return false; state.decoder_eof = false; if (atrac_diag_enabled()) std::cerr << "[atrac] decoder opened source=\"" << state.source_path.string() << "\" sample=" << state.sample_position << "\n"; return true; } std::size_t read_atrac_pcm(AtracContextState &state, std::span output) { if (!open_atrac_decoder(state)) return 0u; const std::size_t total = state.decoder.read(output); if (total < output.size()) state.decoder_eof = true; return total; } std::uint32_t atrac_samples_per_frame(const AtracContextState &state) { return state.header.atrac3plus ? 2048u : 1024u; } std::uint32_t atrac_bitrate_kbps(const AtracContextState &state) { if (state.header.atrac3plus) { const std::uint32_t raw = (static_cast(state.header.block_align) * 352800u) / 1000u; return ((raw >> 11u) + 8u) & 0xFFFFFFF0u; } return (static_cast(state.header.block_align) * 352800u / 1000u + 511u) >> 10u; } struct SubInterruptRecord { std::uint32_t handler{}; std::uint32_t argument{}; bool enabled{}; }; std::unordered_map sub_interrupts; std::uint64_t sub_interrupt_key(std::uint32_t interrupt_number, std::uint32_t sub_number) { return (static_cast(interrupt_number) << 32u) | sub_number; } enum class UtilityStatus : std::uint32_t { None = 0u, Init = 1u, Visible = 2u, Quit = 3u, Finished = 4u, }; struct SavedataUtilityState { UtilityStatus status{UtilityStatus::None}; std::uint32_t parameter_address{}; bool operation_complete{}; }; SavedataUtilityState savedata_utility{}; constexpr std::uint32_t kUtilityCommonResultOffset = 0x1Cu; constexpr std::uint32_t kSavedataModeOffset = 0x30u; constexpr std::uint32_t kSavedataGameNameOffset = 0x3Cu; constexpr std::uint32_t kSavedataSaveNameOffset = 0x4Cu; constexpr std::uint32_t kSavedataFileNameOffset = 0x64u; constexpr std::uint32_t kSavedataDataBufferOffset = 0x74u; constexpr std::uint32_t kSavedataDataBufferSizeOffset = 0x78u; constexpr std::uint32_t kSavedataDataSizeOffset = 0x7Cu; constexpr std::uint32_t kSavedataIcon0Offset = 0x584u; constexpr std::uint32_t kSavedataIcon1Offset = 0x594u; constexpr std::uint32_t kSavedataPic1Offset = 0x5A4u; constexpr std::uint32_t kSavedataSnd0Offset = 0x5B4u; constexpr std::uint32_t kSavedataIdListOffset = 0x5F4u; constexpr std::uint32_t kSavedataParameterMinimumSize = 0x600u; std::string read_fixed_string(const psprecomp::GuestMemory &memory, std::uint32_t address, std::size_t size) { std::string result; result.reserve(size); for (std::size_t index = 0; index < size; ++index) { const char value = static_cast(memory.load8(address + static_cast(index))); if (value == '\0') break; result.push_back(value); } return result; } std::string safe_savedata_component(std::string value) { value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char c) { return c == '/' || c == '\\' || c == ':' || c < 0x20u; }), value.end()); return value; } std::filesystem::path savedata_root(const psprecomp::Runtime &runtime) { return runtime.game_root() / "PSP" / "SAVEDATA"; } std::filesystem::path savedata_directory(const psprecomp::Runtime &runtime, std::uint32_t parameter_address) { const std::string game = safe_savedata_component(read_fixed_string( runtime.memory(), parameter_address + kSavedataGameNameOffset, 13u)); const std::string save = safe_savedata_component(read_fixed_string( runtime.memory(), parameter_address + kSavedataSaveNameOffset, 20u)); return savedata_root(runtime) / (game + save); } bool write_guest_file(psprecomp::Runtime &runtime, const std::filesystem::path &path, std::uint32_t buffer, std::uint32_t size) { if (size == 0u) return true; if (buffer == 0u || !runtime.memory().contains(buffer, size)) return false; std::filesystem::create_directories(path.parent_path()); std::ofstream output(path, std::ios::binary | std::ios::trunc); if (!output) return false; std::vector data(size); for (std::uint32_t index = 0u; index < size; ++index) data[index] = runtime.memory().load8(buffer + index); output.write(reinterpret_cast(data.data()), static_cast(data.size())); return output.good(); } bool write_savedata_auxiliary(psprecomp::Runtime &runtime, std::uint32_t parameter_address, std::uint32_t descriptor_offset, const char *filename) { const std::uint32_t descriptor = parameter_address + descriptor_offset; const std::uint32_t buffer = runtime.memory().load32(descriptor); const std::uint32_t buffer_size = runtime.memory().load32(descriptor + 4u); const std::uint32_t actual_size = runtime.memory().load32(descriptor + 8u); if (buffer == 0u || actual_size == 0u) return true; if (actual_size > buffer_size) return false; return write_guest_file(runtime, savedata_directory(runtime, parameter_address) / filename, buffer, actual_size); } std::uint32_t load_savedata_file(psprecomp::Runtime &runtime, std::uint32_t parameter_address, const std::filesystem::path &path, bool raw_mode) { if (!std::filesystem::is_regular_file(path)) { return raw_mode ? 0x80110329u : 0x80110307u; } std::ifstream input(path, std::ios::binary | std::ios::ate); if (!input) return raw_mode ? 0x80110329u : 0x80110305u; const auto end = input.tellg(); if (end < 0) return 0x80110305u; const auto file_size = static_cast(end); const std::uint32_t destination = runtime.memory().load32(parameter_address + kSavedataDataBufferOffset); const std::uint32_t capacity = runtime.memory().load32(parameter_address + kSavedataDataBufferSizeOffset); if (file_size > capacity || file_size > 0xFFFFFFFFull || (file_size != 0u && (destination == 0u || !runtime.memory().contains(destination, static_cast(file_size))))) { return raw_mode ? 0x80110328u : 0x80110308u; } std::vector bytes(static_cast(file_size)); input.seekg(0, std::ios::beg); if (!bytes.empty()) input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); if (!input && !bytes.empty()) return 0x80110305u; if (!bytes.empty()) runtime.memory().copy_in(destination, bytes); runtime.memory().store32(parameter_address + kSavedataDataSizeOffset, static_cast(bytes.size())); return 0u; } std::uint32_t save_savedata_file(psprecomp::Runtime &runtime, std::uint32_t parameter_address, const std::filesystem::path &path, bool raw_mode) { const std::uint32_t source = runtime.memory().load32(parameter_address + kSavedataDataBufferOffset); const std::uint32_t capacity = runtime.memory().load32(parameter_address + kSavedataDataBufferSizeOffset); const std::uint32_t size = runtime.memory().load32(parameter_address + kSavedataDataSizeOffset); if (size > capacity || (size != 0u && (source == 0u || !runtime.memory().contains(source, size)))) { return raw_mode ? 0x80110328u : 0x80110388u; } if (!write_guest_file(runtime, path, source, size)) return raw_mode ? 0x80110329u : 0x80110385u; if (!raw_mode) { if (!write_savedata_auxiliary(runtime, parameter_address, kSavedataIcon0Offset, "ICON0.PNG") || !write_savedata_auxiliary(runtime, parameter_address, kSavedataIcon1Offset, "ICON1.PMF") || !write_savedata_auxiliary(runtime, parameter_address, kSavedataPic1Offset, "PIC1.PNG") || !write_savedata_auxiliary(runtime, parameter_address, kSavedataSnd0Offset, "SND0.AT3")) { return 0x80110385u; } } return 0u; } std::uint32_t list_savedata_directories(psprecomp::Runtime &runtime, std::uint32_t parameter_address) { const std::uint32_t info = runtime.memory().load32(parameter_address + kSavedataIdListOffset); if (info == 0u || !runtime.memory().contains(info, 12u)) return 0x80110328u; const std::int32_t max_count = static_cast(runtime.memory().load32(info)); const std::uint32_t entries = runtime.memory().load32(info + 8u); if (max_count < 0 || (max_count > 0 && (entries == 0u || !runtime.memory().contains(entries, static_cast(max_count) * 72u)))) { return 0x80110328u; } const std::string game = safe_savedata_component(read_fixed_string( runtime.memory(), parameter_address + kSavedataGameNameOffset, 13u)); std::vector names; const auto root = savedata_root(runtime); if (std::filesystem::is_directory(root)) { for (const auto &entry : std::filesystem::directory_iterator(root)) { if (!entry.is_directory()) continue; const std::string directory_name = entry.path().filename().string(); if (!directory_name.starts_with(game)) continue; names.push_back(directory_name.substr(game.size())); } } std::sort(names.begin(), names.end()); if (names.size() > static_cast(max_count)) names.resize(static_cast(max_count)); for (std::size_t index = 0; index < names.size(); ++index) { const std::uint32_t entry = entries + static_cast(index * 72u); runtime.memory().zero(entry, 72u); runtime.memory().store32(entry, 0x11FFu); std::vector bytes(names[index].begin(), names[index].end()); if (bytes.size() > 19u) bytes.resize(19u); bytes.push_back(0u); runtime.memory().copy_in(entry + 52u, bytes); } runtime.memory().store32(info + 4u, static_cast(names.size())); return 0u; } std::uint64_t directory_size_bytes(const std::filesystem::path &directory) { std::uint64_t total = 0u; if (!std::filesystem::is_directory(directory)) return total; std::error_code error; for (std::filesystem::recursive_directory_iterator it(directory, error), end; it != end && !error; it.increment(error)) { if (it->is_regular_file(error)) total += it->file_size(error); } return total; } void write_small_size_string(psprecomp::GuestMemory &memory, std::uint32_t address, std::uint64_t kilobytes) { const std::string text = kilobytes > 99999u ? "99999KB" : std::to_string(kilobytes) + "KB"; memory.zero(address, 8u); std::vector bytes(text.begin(), text.end()); if (bytes.size() > 7u) bytes.resize(7u); bytes.push_back(0u); memory.copy_in(address, bytes); } void write_used_data_info(psprecomp::GuestMemory &memory, std::uint32_t address, std::uint64_t used_bytes, std::uint32_t cluster_size) { const std::uint64_t clusters = (used_bytes + cluster_size - 1u) / cluster_size; const std::uint64_t used_kb = (used_bytes + 1023u) / 1024u; const std::uint64_t used_32kb = clusters * (cluster_size / 1024u); memory.store32(address + 0u, static_cast(std::min(clusters, 0xFFFFFFFFull))); memory.store32(address + 4u, static_cast(std::min(used_kb, 0xFFFFFFFFull))); write_small_size_string(memory, address + 8u, used_kb); memory.store32(address + 16u, static_cast(std::min(used_32kb, 0xFFFFFFFFull))); write_small_size_string(memory, address + 20u, used_32kb); } std::uint32_t query_savedata_sizes(psprecomp::Runtime &runtime, std::uint32_t parameter_address) { constexpr std::uint32_t cluster_size = 32u * 1024u; const auto root = savedata_root(runtime); std::error_code error; std::filesystem::create_directories(root, error); const auto space = std::filesystem::space(root, error); const std::uint64_t available = error ? 512ull * 1024ull * 1024ull : space.available; const std::uint64_t free_clusters = available / cluster_size; const std::uint64_t free_kb = available / 1024u; const std::uint64_t used = directory_size_bytes(savedata_directory(runtime, parameter_address)); const std::uint32_t ms_free = runtime.memory().load32(parameter_address + 0x5D0u); if (ms_free != 0u) { if (!runtime.memory().contains(ms_free, 20u)) return 0x801103C8u; runtime.memory().store32(ms_free + 0u, cluster_size); runtime.memory().store32(ms_free + 4u, static_cast(std::min(free_clusters, 0xFFFFFFFFull))); runtime.memory().store32(ms_free + 8u, static_cast(std::min(free_kb, 0xFFFFFFFFull))); write_small_size_string(runtime.memory(), ms_free + 12u, free_kb); } const std::uint32_t ms_data = runtime.memory().load32(parameter_address + 0x5D4u); if (ms_data != 0u) { if (!runtime.memory().contains(ms_data, 64u)) return 0x801103C8u; runtime.memory().zero(ms_data, 64u); for (std::uint32_t index = 0u; index < 13u; ++index) runtime.memory().store8(ms_data + index, runtime.memory().load8(parameter_address + kSavedataGameNameOffset + index)); for (std::uint32_t index = 0u; index < 20u; ++index) runtime.memory().store8(ms_data + 16u + index, runtime.memory().load8(parameter_address + kSavedataSaveNameOffset + index)); write_used_data_info(runtime.memory(), ms_data + 36u, used, cluster_size); } const std::uint32_t utility_data = runtime.memory().load32(parameter_address + 0x5D8u); if (utility_data != 0u) { if (!runtime.memory().contains(utility_data, 28u)) return 0x801103C8u; write_used_data_info(runtime.memory(), utility_data, used, cluster_size); } return 0u; } std::uint32_t execute_savedata_operation(psprecomp::Runtime &runtime, std::uint32_t parameter_address) { const std::uint32_t mode = runtime.memory().load32(parameter_address + kSavedataModeOffset); const std::string file_name_value = safe_savedata_component(read_fixed_string( runtime.memory(), parameter_address + kSavedataFileNameOffset, 13u)); const std::string file_name = file_name_value.empty() ? "DATA.BIN" : file_name_value; const auto directory = savedata_directory(runtime, parameter_address); const auto data_path = directory / file_name; switch (mode) { case 0u: // AUTOLOAD case 2u: // LOAD case 4u: // LISTLOAD (selected saveName is already supplied by the game) return load_savedata_file(runtime, parameter_address, data_path, false); case 1u: // AUTOSAVE case 3u: // SAVE case 5u: // LISTSAVE return save_savedata_file(runtime, parameter_address, data_path, false); case 9u: // AUTODELETE case 10u: // DELETE if (!std::filesystem::exists(directory)) return 0x80110347u; return std::filesystem::remove_all(directory) != 0u ? 0u : 0x80110345u; case 11u: // LIST return list_savedata_directories(runtime, parameter_address); case 13u: // MAKEDATASECURE case 14u: // MAKEDATA case 17u: // WRITEDATASECURE case 18u: // WRITEDATA return save_savedata_file(runtime, parameter_address, data_path, true); case 15u: // READDATASECURE case 16u: // READDATA return load_savedata_file(runtime, parameter_address, data_path, true); case 19u: // ERASESECURE case 20u: // ERASE case 21u: // DELETEDATA if (!std::filesystem::is_regular_file(data_path)) return 0x80110329u; return std::filesystem::remove(data_path) ? 0u : 0x80110329u; case 8u: // SIZES return query_savedata_sizes(runtime, parameter_address); case 12u: // FILES case 22u: // GETSIZE return 0u; default: return 0x80110300u; } } struct ControllerState { std::uint32_t sampling_cycle{}; std::uint32_t sampling_mode{}; }; ControllerState controller_state; struct DisplayState { std::uint32_t mode{}; std::uint32_t width{480u}; std::uint32_t height{272u}; std::uint32_t frame_buffer{}; std::uint32_t buffer_width{512u}; std::uint32_t pixel_format{}; std::uint32_t sync_mode{}; }; DisplayState display_state; std::uint64_t display_vblank_index{}; constexpr std::uint64_t kVblankPeriodUs = 16683u; struct GeCallbackRecord { std::uint32_t signal_function{}; std::uint32_t signal_argument{}; std::uint32_t finish_function{}; std::uint32_t finish_argument{}; }; struct GeCallbackTable { std::int32_t next_uid{0x700}; std::unordered_map callbacks; }; GeCallbackTable ge_callback_table; std::int32_t ge_next_list_id{1}; std::unordered_map> async_return_frames; std::unordered_map> pending_ge_callbacks; std::uint64_t virtual_time_us{}; std::uint64_t umd_stream_flag_last_rearm_us{}; constexpr std::uint32_t kGameRenderWidthAddress = 0x08B5698Cu; std::uint32_t audio_remaining_samples(const AudioChannelState &channel) { if (!channel.reserved || channel.busy_until_us <= virtual_time_us) return 0u; const std::uint64_t remaining_us = channel.busy_until_us - virtual_time_us; const std::uint64_t samples = (remaining_us * 44100u + 999999u) / 1000000u; return static_cast(std::min(samples, channel.sample_count)); } std::uint64_t audio_queue_buffer(AudioChannelState &channel, std::uint32_t frames) { const auto elapsed_us = [](std::uint64_t sample_frames) { return (sample_frames * 1000000ull) / 44100ull; }; std::uint64_t start = channel.queue_anchor_us + elapsed_us(channel.queued_frames); if (!channel.queue_active || start < virtual_time_us) { channel.queue_active = true; channel.queue_anchor_us = virtual_time_us; channel.queued_frames = 0u; start = virtual_time_us; } channel.queued_frames += frames; channel.busy_until_us = channel.queue_anchor_us + elapsed_us(channel.queued_frames); return start; } void set_success(psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, 0u); } std::uint64_t system_time_microseconds() { return virtual_time_us; } void enqueue_continuation(std::int32_t uid, const psprecomp::AllegrexContext &context) { auto thread = thread_table.threads.find(uid); if (thread != thread_table.threads.end()) { thread->second.state = ThreadState::Ready; thread->second.suspended_context = context; if (thread->second.externally_suspended) { thread_table.continuations.erase( std::remove_if(thread_table.continuations.begin(), thread_table.continuations.end(), [uid](const ThreadContinuation &item) { return item.uid == uid; }), thread_table.continuations.end()); return; } } const auto existing = std::find_if( thread_table.continuations.begin(), thread_table.continuations.end(), [uid](const ThreadContinuation &item) { return item.uid == uid; }); if (existing != thread_table.continuations.end()) { existing->context = context; } else { thread_table.continuations.push_back( ThreadContinuation{uid, context, thread_table.next_ready_sequence++}); } } bool activate_next_thread(psprecomp::AllegrexContext &ctx, const char *reason); bool event_flag_matches(const EventFlagRecord &flag, std::uint32_t requested, std::uint32_t mode); void consume_event_flag(EventFlagRecord &flag, std::uint32_t requested, std::uint32_t mode); void maybe_rearm_umd_stream_flag() { if (std::getenv("LCS_NO_REARM") != nullptr) return; static const std::uint64_t rearm_interval_us = [] { const char *text = std::getenv("LCS_REARM_US"); if (text == nullptr || *text == '\0') return std::uint64_t{5000u}; return static_cast(std::strtoull(text, nullptr, 0)); }(); if (virtual_time_us - umd_stream_flag_last_rearm_us < rearm_interval_us) return; for (auto &[uid, flag] : event_flag_table.flags) { if (flag.name != "UmdStreamEventFlag") continue; umd_stream_flag_last_rearm_us = virtual_time_us; if ((flag.current_pattern & 0x1u) != 0u) return; flag.current_pattern |= 0x1u; auto waiter = flag.waiters.begin(); while (waiter != flag.waiters.end()) { if (!event_flag_matches(flag, waiter->requested, waiter->mode)) { ++waiter; continue; } consume_event_flag(flag, waiter->requested, waiter->mode); waiter->context.set_gpr(2, 0u); enqueue_continuation(waiter->uid, waiter->context); waiter = flag.waiters.erase(waiter); } flag.current_pattern &= ~0x1u; return; } } std::uint32_t thread_priority(std::int32_t uid) { const auto found = thread_table.threads.find(uid); return found != thread_table.threads.end() ? found->second.priority : 0xFFFFFFFFu; } auto best_ready_thread() { return std::min_element( thread_table.continuations.begin(), thread_table.continuations.end(), [](const ThreadContinuation &left, const ThreadContinuation &right) { const std::uint32_t left_priority = thread_priority(left.uid); const std::uint32_t right_priority = thread_priority(right.uid); if (left_priority != right_priority) return left_priority < right_priority; return left.ready_sequence < right.ready_sequence; }); } bool preempt_if_higher_priority(psprecomp::AllegrexContext &ctx, const char *reason) { const auto current = thread_table.threads.find(thread_table.current_uid); if (current == thread_table.threads.end() || current->second.state != ThreadState::Running) return false; const auto best = best_ready_thread(); if (best == thread_table.continuations.end() || thread_priority(best->uid) >= thread_priority(thread_table.current_uid)) { return false; } const std::int32_t caller_uid = thread_table.current_uid; psprecomp::AllegrexContext caller = ctx; caller.pc = ctx.gpr[31]; enqueue_continuation(caller_uid, caller); return activate_next_thread(ctx, reason); } void promote_expired_delays() { struct ExpiredDelay { std::int32_t uid{}; std::uint64_t deadline{}; std::uint64_t sequence{}; }; std::vector expired; expired.reserve(thread_table.threads.size()); for (const auto &[uid, thread] : thread_table.threads) { if (thread.state == ThreadState::Delayed && thread.delay_until_us <= virtual_time_us) expired.push_back(ExpiredDelay{uid, thread.delay_until_us, thread.delay_sequence}); } std::sort(expired.begin(), expired.end(), [](const ExpiredDelay &left, const ExpiredDelay &right) { if (left.deadline != right.deadline) return left.deadline < right.deadline; if (left.sequence != right.sequence) return left.sequence < right.sequence; return left.uid < right.uid; }); for (const ExpiredDelay &item : expired) { const auto thread = thread_table.threads.find(item.uid); if (thread != thread_table.threads.end()) enqueue_continuation(item.uid, thread->second.suspended_context); } } bool speed_diag_enabled() { static const bool enabled = std::getenv("PSPRECOMP_REALTIME_SPEED_DIAG") != nullptr; return enabled; } std::unordered_map g_speed_thread_ns; std::chrono::steady_clock::time_point g_speed_thread_mark{}; void note_thread_switch() { if (!speed_diag_enabled()) return; const auto now = std::chrono::steady_clock::now(); if (g_speed_thread_mark.time_since_epoch().count() != 0) { g_speed_thread_ns[thread_table.current_uid] += static_cast( std::chrono::duration_cast(now - g_speed_thread_mark).count()); } g_speed_thread_mark = now; } bool activate_next_thread(psprecomp::AllegrexContext &ctx, const char *reason) { (void)reason; note_thread_switch(); maybe_rearm_umd_stream_flag(); promote_expired_delays(); thread_table.continuations.erase( std::remove_if(thread_table.continuations.begin(), thread_table.continuations.end(), [](const ThreadContinuation &item) { const auto thread = thread_table.threads.find(item.uid); return thread == thread_table.threads.end() || thread->second.externally_suspended; }), thread_table.continuations.end()); if (thread_table.continuations.empty()) { std::uint64_t earliest = UINT64_MAX; for (const auto &[uid, thread] : thread_table.threads) { (void)uid; if (thread.state == ThreadState::Delayed) earliest = std::min(earliest, thread.delay_until_us); } if (earliest != UINT64_MAX) { virtual_time_us = std::max(virtual_time_us, earliest); promote_expired_delays(); } } if (thread_table.continuations.empty()) return false; const auto selected = best_ready_thread(); ThreadContinuation continuation = *selected; thread_table.continuations.erase(selected); thread_table.current_uid = continuation.uid; if (auto thread = thread_table.threads.find(continuation.uid); thread != thread_table.threads.end()) { thread->second.state = ThreadState::Running; psprecomp::set_runtime_thread_identity(continuation.uid, thread->second.name); } ctx = continuation.context; return true; } psprecomp::AllegrexContext make_wait_context(const psprecomp::AllegrexContext &ctx) { psprecomp::AllegrexContext suspended = ctx; suspended.set_gpr(2, 0u); suspended.pc = ctx.gpr[31]; return suspended; } void check_wall_clock_limit(psprecomp::Runtime &runtime, std::uint64_t sample_mask); bool delay_current_thread(psprecomp::Runtime &runtime, psprecomp::AllegrexContext &ctx, std::uint32_t delay_microseconds, std::uint32_t return_value = 0u) { check_wall_clock_limit(runtime, 0xFFu); auto current = thread_table.threads.find(thread_table.current_uid); if (current == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return false; } psprecomp::AllegrexContext suspended = make_wait_context(ctx); suspended.set_gpr(2, return_value); current->second.state = ThreadState::Delayed; current->second.suspended_context = suspended; current->second.delay_until_us = virtual_time_us + delay_microseconds; current->second.delay_sequence = thread_table.next_delay_sequence++; if (!activate_next_thread(ctx, "delay")) { runtime.stop("PSP scheduler deadlock while delaying thread"); return false; } return true; } bool suspend_current_thread(psprecomp::Runtime &runtime, psprecomp::AllegrexContext &ctx, const psprecomp::AllegrexContext &suspended, const std::string &reason) { if (auto current = thread_table.threads.find(thread_table.current_uid); current != thread_table.threads.end()) { current->second.state = ThreadState::Sleeping; current->second.suspended_context = suspended; } if (!activate_next_thread(ctx, reason.c_str())) { runtime.stop("PSP scheduler deadlock while waiting for " + reason); return false; } return true; } bool sleep_current_thread(psprecomp::Runtime &runtime, psprecomp::AllegrexContext &ctx) { auto current = thread_table.threads.find(thread_table.current_uid); if (current == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return false; } if (current->second.wakeup_count != 0u) { --current->second.wakeup_count; set_success(ctx); return true; } const psprecomp::AllegrexContext suspended = make_wait_context(ctx); current->second.state = ThreadState::Sleeping; current->second.suspended_context = suspended; if (!activate_next_thread(ctx, "sleep")) { runtime.stop("PSP scheduler deadlock: every thread is sleeping"); return false; } return true; } std::uint32_t wake_thread(std::int32_t uid) { const auto found = thread_table.threads.find(uid); if (found == thread_table.threads.end()) return 0x80020198u; ThreadRecord &thread = found->second; if (thread.state == ThreadState::Completed || thread.state == ThreadState::Created) return 0x800201A2u; if (thread.state == ThreadState::Sleeping) { enqueue_continuation(uid, thread.suspended_context); } else { ++thread.wakeup_count; } return 0u; } void release_thread_stack(const ThreadRecord &thread) { if (thread.stack_bottom == 0u || thread.stack_top <= thread.stack_bottom) return; thread_table.free_stacks.push_back({thread.stack_bottom, thread.stack_top}); std::sort(thread_table.free_stacks.begin(), thread_table.free_stacks.end(), [](const FreeThreadStack &left, const FreeThreadStack &right) { return left.bottom < right.bottom; }); std::vector merged; for (const FreeThreadStack block : thread_table.free_stacks) { if (!merged.empty() && block.bottom <= merged.back().top) { merged.back().top = std::max(merged.back().top, block.top); } else { merged.push_back(block); } } thread_table.free_stacks = std::move(merged); for (;;) { const auto adjacent = std::find_if(thread_table.free_stacks.begin(), thread_table.free_stacks.end(), [](const FreeThreadStack &block) { return block.bottom == thread_table.next_stack_top; }); if (adjacent == thread_table.free_stacks.end()) break; thread_table.next_stack_top = adjacent->top; thread_table.free_stacks.erase(adjacent); } } bool allocate_thread_stack(std::uint32_t stack_size, std::uint32_t &bottom, std::uint32_t &top) { auto best = thread_table.free_stacks.end(); for (auto it = thread_table.free_stacks.begin(); it != thread_table.free_stacks.end(); ++it) { const std::uint32_t size = it->top - it->bottom; if (size < stack_size) continue; if (best == thread_table.free_stacks.end() || size < best->top - best->bottom) best = it; } if (best != thread_table.free_stacks.end()) { top = best->top; bottom = top - stack_size; if (bottom == best->bottom) thread_table.free_stacks.erase(best); else best->top = bottom; return true; } top = thread_table.next_stack_top & ~0xFFu; if (top < stack_size) return false; bottom = top - stack_size; if (bottom < partition_table.next_address) return false; thread_table.next_stack_top = bottom; return true; } void remove_thread_from_wait_queues(std::int32_t uid) { for (auto &[semaphore_uid, semaphore] : semaphore_table.semaphores) { (void)semaphore_uid; semaphore.waiters.erase(std::remove_if(semaphore.waiters.begin(), semaphore.waiters.end(), [uid](const SemaphoreWaiter &waiter) { return waiter.uid == uid; }), semaphore.waiters.end()); } for (auto &[flag_uid, flag] : event_flag_table.flags) { (void)flag_uid; flag.waiters.erase(std::remove_if(flag.waiters.begin(), flag.waiters.end(), [uid](const EventFlagWaiter &waiter) { return waiter.uid == uid; }), flag.waiters.end()); } for (auto &[target_uid, waiters] : thread_table.thread_end_waiters) { (void)target_uid; waiters.erase(std::remove_if(waiters.begin(), waiters.end(), [uid](const ThreadContinuation &waiter) { return waiter.uid == uid; }), waiters.end()); } std::erase_if(thread_table.thread_end_waiters, [](const auto &entry) { return entry.second.empty(); }); std::erase_if(callback_table.callbacks, [uid](const auto &entry) { return entry.second.owner_uid == uid; }); } void wake_thread_end_waiters(std::int32_t completed_uid, std::uint32_t result = 0u) { const auto found = thread_table.thread_end_waiters.find(completed_uid); if (found == thread_table.thread_end_waiters.end()) return; for (auto &waiter : found->second) { waiter.context.set_gpr(2, result); enqueue_continuation(waiter.uid, waiter.context); } thread_table.thread_end_waiters.erase(found); } void complete_current_thread(psprecomp::Runtime &runtime, psprecomp::AllegrexContext &ctx) { const std::int32_t completed_uid = thread_table.current_uid; if (std::getenv("LCS_THREAD_DIAG") != nullptr) { const auto found = thread_table.threads.find(completed_uid); std::cerr << "[thread] exit uid=" << completed_uid << " name=\"" << (found != thread_table.threads.end() ? found->second.name : std::string("?")) << "\" t=" << virtual_time_us << " vblank=" << display_vblank_index << " v0=" << psprecomp::hex32(ctx.gpr[2]) << " a0=" << psprecomp::hex32(ctx.gpr[4]) << " ra=" << psprecomp::hex32(ctx.gpr[31]) << "\n"; } if (auto current = thread_table.threads.find(completed_uid); current != thread_table.threads.end()) current->second.state = ThreadState::Completed; thread_table.continuations.erase( std::remove_if(thread_table.continuations.begin(), thread_table.continuations.end(), [completed_uid](const ThreadContinuation &item) { return item.uid == completed_uid; }), thread_table.continuations.end()); async_return_frames.erase(completed_uid); wake_thread_end_waiters(completed_uid); if (!activate_next_thread(ctx, "thread-complete")) { ctx.set_gpr(2, 0u); runtime.stop("All PSP threads completed"); } } void lcs_module_thread_return(psprecomp::Runtime &runtime, psprecomp::AllegrexContext &ctx) { complete_current_thread(runtime, ctx); } bool dispatch_vblank_interrupt(psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx, std::uint32_t delay_us); bool dispatch_vblank_interrupt_if_due(psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx, std::uint32_t delay_us); void hang_trace(const std::string &line); void lcs_callback_return(psprecomp::Runtime &runtime, psprecomp::AllegrexContext &ctx) { const std::int32_t uid = thread_table.current_uid; const auto found = async_return_frames.find(uid); if (found == async_return_frames.end() || found->second.empty()) { runtime.stop("PSP callback return without a saved thread context"); return; } const AsyncReturnFrame frame = found->second.back(); found->second.pop_back(); if (found->second.empty()) async_return_frames.erase(found); if (frame.kind == AsyncReturnKind::GeFinishThenDelay) { ctx = frame.resume; if (frame.vblank_wait ? dispatch_vblank_interrupt(runtime, ctx, frame.delay_us) : dispatch_vblank_interrupt_if_due(runtime, ctx, frame.delay_us)) return; (void)delay_current_thread(runtime, ctx, frame.delay_us); return; } if (frame.kind == AsyncReturnKind::SubInterrupt) { ctx = frame.resume; return; } if (frame.kind == AsyncReturnKind::MpegRingbuffer) { const auto produced = static_cast(ctx.gpr[2]); if (std::getenv("LCS_MPEG_DIAG") != nullptr) std::cerr << "[mpeg] ring callback returned " << produced << "\n"; auto &memory = runtime.memory(); if (produced > 0 && memory.contains(frame.ring, 48u)) { const std::int32_t packets = static_cast(memory.load32(frame.ring)); const std::int32_t used = static_cast(memory.load32(frame.ring + 12u)); const std::int32_t write_position = static_cast(memory.load32(frame.ring + 8u)); if (packets > 0) { memory.store32(frame.ring + 12u, static_cast(std::min(packets, used + produced))); memory.store32(frame.ring + 8u, static_cast((write_position + produced) % packets)); } } ctx = frame.resume; ctx.set_gpr(2, static_cast(std::max(0, produced))); return; } ctx = frame.resume; } bool deliver_pending_ge_callback(psprecomp::AllegrexContext &ctx, std::uint32_t delay_us, bool vblank_wait = false) { const auto pending = pending_ge_callbacks.find(thread_table.current_uid); if (pending == pending_ge_callbacks.end() || pending->second.empty()) return false; auto &frames = async_return_frames[thread_table.current_uid]; if (!frames.empty()) return false; const PendingGeCallback callback = pending->second.front(); pending->second.pop_front(); AsyncReturnFrame frame{}; frame.resume = ctx; frame.callback_uid = callback.callback_uid; frame.kind = AsyncReturnKind::GeFinishThenDelay; frame.delay_us = delay_us; frame.vblank_wait = vblank_wait; frames.push_back(frame); hang_trace("ge-callback fn=" + psprecomp::hex32(callback.function) + " finish_arg=" + std::to_string(callback.finish_argument) + " vblank_wait=" + std::to_string(vblank_wait ? 1 : 0)); ctx.set_gpr(4, callback.finish_argument); ctx.set_gpr(5, callback.user_argument); ctx.set_gpr(31, 0x00000004u); ctx.pc = callback.function; return true; } std::uint64_t g_vblank_interrupt_due_us = 0u; bool vblank_interrupt_enabled() { static const bool enabled = std::getenv("LCS_NO_VBLANK_INTERRUPT") == nullptr; return enabled; } bool in_vblank_interrupt() { const auto found = async_return_frames.find(thread_table.current_uid); return found != async_return_frames.end() && !found->second.empty() && found->second.back().kind == AsyncReturnKind::SubInterrupt; } bool dispatch_vblank_interrupt(psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx, std::uint32_t delay_us) { if (!vblank_interrupt_enabled()) return false; const auto interrupt = sub_interrupts.find(sub_interrupt_key(30u, 15u)); if (interrupt == sub_interrupts.end() || !interrupt->second.enabled || interrupt->second.handler == 0u) return false; const auto current = thread_table.threads.find(thread_table.current_uid); if (current == thread_table.threads.end()) return false; auto &frames = async_return_frames[thread_table.current_uid]; if (!frames.empty()) return false; AsyncReturnFrame frame{}; frame.resume = make_wait_context(ctx); frame.kind = AsyncReturnKind::SubInterrupt; frames.push_back(frame); psprecomp::AllegrexContext handler = ctx; handler.set_gpr(4, 15u); handler.set_gpr(5, interrupt->second.argument); handler.set_gpr(31, 0x00000004u); handler.pc = interrupt->second.handler; current->second.state = ThreadState::Delayed; current->second.suspended_context = handler; current->second.delay_until_us = virtual_time_us + delay_us; current->second.delay_sequence = thread_table.next_delay_sequence++; g_vblank_interrupt_due_us = virtual_time_us + delay_us + kVblankPeriodUs; hang_trace("vblank-interrupt delay=" + std::to_string(delay_us) + " resume=" + psprecomp::hex32(frame.resume.pc)); if (!activate_next_thread(ctx, "vblank-interrupt")) rt.stop("PSP scheduler deadlock while waiting for the VBlank interrupt"); return true; } bool dispatch_vblank_interrupt_if_due(psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx, std::uint32_t delay_us) { static const bool disabled = std::getenv("LCS_NO_DELAY_VBLANK") != nullptr; if (disabled || virtual_time_us + delay_us < g_vblank_interrupt_due_us) return false; return dispatch_vblank_interrupt(rt, ctx, delay_us); } bool try_dispatch_pending_callback(psprecomp::AllegrexContext &ctx) { auto pending = std::find_if(callback_table.callbacks.begin(), callback_table.callbacks.end(), [](const auto &item) { return item.second.owner_uid == thread_table.current_uid && item.second.notify_count != 0u && item.second.function != 0u; }); if (pending == callback_table.callbacks.end()) return false; CallbackRecord &callback = pending->second; const std::uint32_t count = callback.notify_count; const std::uint32_t argument = callback.notify_argument; callback.notify_count = 0u; psprecomp::AllegrexContext resume = ctx; resume.pc = ctx.gpr[31]; resume.set_gpr(2, 1u); auto &frames = async_return_frames[thread_table.current_uid]; if (!frames.empty()) return false; frames.push_back(AsyncReturnFrame{resume, pending->first}); ctx.set_gpr(4, count); ctx.set_gpr(5, argument); ctx.set_gpr(6, callback.common); ctx.set_gpr(31, 0x00000004u); ctx.pc = callback.function; return true; } bool event_flag_matches(const EventFlagRecord &flag, std::uint32_t requested, std::uint32_t mode) { if ((mode & 1u) != 0u) return (flag.current_pattern & requested) != 0u; return (flag.current_pattern & requested) == requested; } void consume_event_flag(EventFlagRecord &flag, std::uint32_t requested, std::uint32_t mode) { if ((mode & 0x20u) != 0u) flag.current_pattern &= ~requested; if ((mode & 0x10u) != 0u) flag.current_pattern = 0u; } struct DeferredIoResume { std::int32_t handoff_uid{}; std::uint32_t handoff_pc{}; std::uint32_t release_pc{}; }; std::unordered_map deferred_io_resumes; void lcs_post_dispatch_hook(psprecomp::Runtime &, psprecomp::AllegrexContext &, std::uint32_t dispatch_pc, std::int32_t dispatch_thread_uid); void refresh_lcs_post_dispatch_hook() { psprecomp::set_runtime_post_dispatch_hook( deferred_io_resumes.empty() ? nullptr : &lcs_post_dispatch_hook); } void lcs_post_dispatch_hook(psprecomp::Runtime &, psprecomp::AllegrexContext &, std::uint32_t dispatch_pc, std::int32_t dispatch_thread_uid) { if (deferred_io_resumes.empty()) return; std::vector completed; for (const auto &[worker_uid, barrier] : deferred_io_resumes) { if (dispatch_thread_uid == barrier.handoff_uid && dispatch_pc == barrier.release_pc) completed.push_back(worker_uid); } if (completed.empty()) return; for (const std::int32_t worker_uid : completed) { const auto worker = thread_table.threads.find(worker_uid); if (worker != thread_table.threads.end() && worker->second.state == ThreadState::IoDeferred) { if (std::getenv("LCS_IO_HANDOFF_DIAG") != nullptr) { std::cerr << "[io-handoff] release worker=" << worker_uid << " release_pc=" << psprecomp::hex32(dispatch_pc) << "\n"; } enqueue_continuation(worker_uid, worker->second.suspended_context); } deferred_io_resumes.erase(worker_uid); } refresh_lcs_post_dispatch_hook(); } bool defer_current_thread_for_io_handoff(psprecomp::AllegrexContext &ctx, std::uint32_t return_value) { const std::int32_t worker_uid = thread_table.current_uid; const auto worker = thread_table.threads.find(worker_uid); if (worker == thread_table.threads.end()) { ctx.set_gpr(2, return_value); return false; } if (best_ready_thread() == thread_table.continuations.end()) { ctx.set_gpr(2, return_value); return false; } psprecomp::AllegrexContext suspended = make_wait_context(ctx); suspended.set_gpr(2, return_value); worker->second.state = ThreadState::IoDeferred; worker->second.suspended_context = suspended; if (!activate_next_thread(ctx, "io-handoff")) { worker->second.state = ThreadState::Running; ctx = suspended; return false; } deferred_io_resumes[worker_uid] = DeferredIoResume{thread_table.current_uid, ctx.pc, ctx.pc}; refresh_lcs_post_dispatch_hook(); if (std::getenv("LCS_IO_HANDOFF_DIAG") != nullptr) { std::cerr << "[io-handoff] arm worker=" << worker_uid << " submitter=" << thread_table.current_uid << " release_pc=" << psprecomp::hex32(ctx.pc) << "\n"; } return true; } void dump_ram_if_requested(const psprecomp::GuestMemory &memory) { struct Config { std::filesystem::path directory; std::uint64_t start{}; std::uint64_t end{}; std::uint64_t interval{1u}; bool dump_vram{}; bool enabled{}; }; static const auto parse_u64 = [](const char *name, std::uint64_t fallback) { const char *text = std::getenv(name); if (text == nullptr || *text == '\0') return fallback; return static_cast(std::strtoull(text, nullptr, 0)); }; static const Config config = [] { Config value{}; const char *directory = std::getenv("PSPRECOMP_RAM_DUMP_DIR"); if (directory == nullptr || *directory == '\0') return value; value.directory = directory; value.start = parse_u64("PSPRECOMP_RAM_DUMP_START_VBLANK", 0u); value.end = parse_u64("PSPRECOMP_RAM_DUMP_END_VBLANK", value.start); value.interval = std::max(1u, parse_u64("PSPRECOMP_RAM_DUMP_INTERVAL", 1u)); value.dump_vram = parse_u64("PSPRECOMP_RAM_DUMP_VRAM", 0u) != 0u; value.enabled = true; return value; }(); if (!config.enabled || display_vblank_index < config.start || display_vblank_index > config.end || ((display_vblank_index - config.start) % config.interval) != 0u) { return; } std::filesystem::create_directories(config.directory); std::ostringstream stem; stem << "ram_vblank_" << std::setw(6) << std::setfill('0') << display_vblank_index; const auto write_bytes = [](const std::filesystem::path &path, const std::vector &bytes) { std::ofstream output(path, std::ios::binary | std::ios::trunc); if (!output) return; output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); }; const std::filesystem::path ram_path = config.directory / (stem.str() + ".bin"); write_bytes(ram_path, memory.bytes()); if (config.dump_vram) write_bytes(config.directory / (stem.str() + ".vram.bin"), memory.vram_bytes()); std::cerr << "[ram-dump] vblank=" << display_vblank_index << " path=" << ram_path.string() << " bytes=" << memory.bytes().size() << "\n"; } std::uint64_t starvation_tick_microseconds = 1u; double wall_clock_limit_seconds = 0.0; std::chrono::steady_clock::time_point wall_clock_start; std::uint32_t throttle_vblank_to_real_time() { static const bool uncapped = std::getenv("LCS_UNCAPPED") != nullptr; if (uncapped) return 0u; static const bool skip_time = std::getenv("LCS_NO_VBLANK_SKIP") == nullptr; constexpr std::uint32_t kMaxSkippedVblanks = 6u; static std::chrono::steady_clock::time_point next_vblank{}; const auto period = std::chrono::microseconds(kVblankPeriodUs); const auto now = std::chrono::steady_clock::now(); if (next_vblank == std::chrono::steady_clock::time_point{}) { next_vblank = now + period; return 0u; } if (now < next_vblank) { std::this_thread::sleep_until(next_vblank); next_vblank += period; return 0u; } if (!skip_time) { next_vblank = now > next_vblank + period * 8 ? now + period : next_vblank + period; return 0u; } const auto behind = static_cast((now - next_vblank) / period); const auto skipped = static_cast(std::min(behind, kMaxSkippedVblanks)); next_vblank = behind > kMaxSkippedVblanks ? now + period : next_vblank + period * (skipped + 1u); return skipped; } std::atomic g_speed_ge_list_ns{}; std::atomic g_speed_gpu_finish_ns{}; std::atomic g_speed_throttle_ns{}; std::atomic g_speed_ge_wait_ns{}; struct GeWorker { std::mutex mutex; std::condition_variable cv; std::thread thread; std::deque> tasks; bool busy{}; bool stop{}; bool started{}; std::atomic pending_presents{0u}; }; GeWorker ge_worker; bool ge_async_enabled() { static const bool enabled = [] { const char *text = std::getenv("LCS_GE_ASYNC"); return text == nullptr || *text == '\0' || std::strcmp(text, "0") != 0; }(); return enabled; } void ge_worker_main() { for (;;) { std::function task; { std::unique_lock lock(ge_worker.mutex); ge_worker.cv.wait(lock, [] { return ge_worker.stop || !ge_worker.tasks.empty(); }); if (ge_worker.tasks.empty()) break; task = std::move(ge_worker.tasks.front()); ge_worker.tasks.pop_front(); ge_worker.busy = true; } task(); { std::lock_guard lock(ge_worker.mutex); ge_worker.busy = false; } ge_worker.cv.notify_all(); } } void ge_worker_submit(std::function task) { if (!ge_async_enabled()) { task(); return; } { std::lock_guard lock(ge_worker.mutex); if (!ge_worker.started) { ge_worker.started = true; ge_worker.stop = false; ge_worker.thread = std::thread(&ge_worker_main); } ge_worker.tasks.push_back(std::move(task)); } ge_worker.cv.notify_all(); } void ge_worker_wait_idle() { std::unique_lock lock(ge_worker.mutex); if (!ge_worker.started) return; if (ge_worker.tasks.empty() && !ge_worker.busy) return; const auto started = std::chrono::steady_clock::now(); ge_worker.cv.wait(lock, [] { return ge_worker.tasks.empty() && !ge_worker.busy; }); g_speed_ge_wait_ns += static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now() - started).count()); } void ge_worker_stop() { std::thread worker; { std::lock_guard lock(ge_worker.mutex); if (!ge_worker.started) return; ge_worker.stop = true; worker = std::move(ge_worker.thread); } ge_worker.cv.notify_all(); if (worker.joinable()) worker.join(); std::lock_guard lock(ge_worker.mutex); ge_worker.started = false; ge_worker.stop = false; ge_worker.tasks.clear(); ge_worker.pending_presents.store(0u); } struct GeWorkerLifetime { ~GeWorkerLifetime() { ge_worker_stop(); } }; GeWorkerLifetime ge_worker_lifetime; struct PresentRequest { std::uint64_t vblank{}; std::uint32_t display_buffer{}; std::uint32_t display_stride{}; std::uint32_t pixel_format{}; std::uint32_t display_width{}; std::uint32_t display_height{}; std::uint32_t game_width{}; std::uint32_t game_height{}; }; void cap_frame_rate(std::uint32_t list_address) { static const std::uint32_t fps = [] { const char *text = std::getenv("LCS_FPS_CAP"); if (text == nullptr || *text == '\0') return 0u; return static_cast(std::strtoul(text, nullptr, 10)); }(); if (fps == 0u) return; static std::uint32_t last_address = 0u; static std::chrono::steady_clock::time_point deadline{}; if (list_address == last_address) return; last_address = list_address; const auto period = std::chrono::duration_cast( std::chrono::duration(1.0 / static_cast(fps))); const auto now = std::chrono::steady_clock::now(); if (deadline == std::chrono::steady_clock::time_point{} || now > deadline + period * 4) { deadline = now + period; return; } if (now < deadline) std::this_thread::sleep_until(deadline); deadline += period; } bool g_ge_list_since_finish = false; std::uint32_t g_ge_last_list_address = 0u; bool ge_frame_split_enabled() { static const bool enabled = std::getenv("LCS_GE_NO_FRAME_SPLIT") == nullptr; return enabled; } void execute_ge_list_frame(psprecomp::Runtime &rt, std::uint32_t list_address, std::uint64_t vblank) { if (ge_frame_split_enabled() && g_ge_list_since_finish && list_address != g_ge_last_list_address) (void)ge_gpu_backend_finish_color_frame(vblank); execute_ge_list_rendered(rt.memory(), list_address); g_ge_list_since_finish = true; g_ge_last_list_address = list_address; } void report_timestep(psprecomp::Runtime &rt) { static const bool enabled = std::getenv("LCS_TIMESTEP_DIAG") != nullptr; if (!enabled || !rt.memory().contains(0x08B5E030u, 4u)) return; static auto window_start = std::chrono::steady_clock::now(); static double timestep_sum = 0.0; static std::uint32_t frames = 0u; timestep_sum += std::bit_cast(rt.memory().load32(0x08B5E030u)); ++frames; const auto now = std::chrono::steady_clock::now(); const double seconds = std::chrono::duration(now - window_start).count(); if (seconds < 1.0) return; std::cerr << "[timestep] fps=" << frames / seconds << " avg=" << timestep_sum / frames << " per_second=" << timestep_sum / seconds << " (50 = real time)\n"; window_start = now; timestep_sum = 0.0; frames = 0u; } void present_frame(psprecomp::Runtime &rt, const PresentRequest &request) { report_timestep(rt); std::uint32_t present_buffer = request.display_buffer; std::uint32_t present_stride = request.display_stride; static const bool keep_display_buffer = std::getenv("LCS_NO_PRESENT_RENDER_TARGET") != nullptr; const std::uint32_t render_target = rendered_render_target(); const std::uint32_t render_stride = rendered_render_stride(); if (!keep_display_buffer && render_target != 0u && render_stride != 0u && render_target != request.display_buffer) { present_buffer = render_target; present_stride = render_stride; } if (std::getenv("LCS_PRESENT_DIAG") != nullptr) { static std::uint32_t reported = 0u; if (reported != present_buffer) { reported = present_buffer; std::cerr << "[present] buffer=" << psprecomp::hex32(present_buffer) << " stride=" << present_stride << " display_fb=" << psprecomp::hex32(request.display_buffer) << " render_target=" << psprecomp::hex32(render_target) << "\n"; } } std::uint32_t present_width = request.display_width; std::uint32_t present_height = request.display_height; if (present_buffer != request.display_buffer && request.game_width >= 240u && request.game_width <= present_stride && request.game_height >= 136u && request.game_height <= 512u) { present_width = request.game_width; present_height = request.game_height; } ge_gpu_backend_set_display_framebuffer(present_buffer, present_width, present_height); fps_overlay_render_frame(present_buffer, present_width, present_height); const auto finish_started = std::chrono::steady_clock::now(); const bool gpu_frame_ready = ge_gpu_backend_finish_color_frame(request.vblank); g_ge_list_since_finish = false; g_speed_gpu_finish_ns += static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now() - finish_started).count()); static std::uint64_t vblanks_since_gpu_frame = 0u; static bool holding_gpu_frame = false; if (gpu_frame_ready) { holding_gpu_frame = true; vblanks_since_gpu_frame = 0u; } else if (holding_gpu_frame && ++vblanks_since_gpu_frame > 4u) { holding_gpu_frame = false; } bool presented_gpu_frame = false; if (ge_gpu_backend_presents_directly()) { ge_gpu_backend_mark_window_presented(); presented_gpu_frame = true; } else if ((gpu_frame_ready || holding_gpu_frame) && ge_gpu_backend_active()) { const GeGpuBackendReport gpu = ge_gpu_backend_report(); const std::span rgba = ge_gpu_backend_game_frame_rgba(); if (!rgba.empty()) { display_window_present_rgba(rgba, gpu.offscreen_width, gpu.offscreen_height); ge_gpu_backend_mark_window_presented(); presented_gpu_frame = true; } } if (std::getenv("LCS_PRESENT_DIAG") != nullptr) { static std::uint64_t gpu_presents = 0u, software_presents = 0u; if (presented_gpu_frame) ++gpu_presents; else ++software_presents; if (((gpu_presents + software_presents) % 120u) == 0u) { const std::span frame = ge_gpu_backend_game_frame_rgba(); std::size_t non_black = 0u; for (std::size_t index = 0u; index + 3u < frame.size(); index += 4u) { if (frame[index] != std::byte{0} || frame[index + 1u] != std::byte{0} || frame[index + 2u] != std::byte{0}) ++non_black; } std::cerr << "[present] gpu=" << gpu_presents << " software=" << software_presents << " direct=" << ge_gpu_backend_presents_directly() << " readback_bytes=" << frame.size() << " non_black=" << non_black << "\n"; } } if (!presented_gpu_frame) { display_window_present(rt, present_buffer, present_stride, request.pixel_format, present_width, present_height); } } void report_realtime_speed_if_requested(const psprecomp::Runtime &runtime, std::uint64_t vblank_index) { if (!speed_diag_enabled()) return; static const std::uint64_t interval = [] { const char *text = std::getenv("PSPRECOMP_REALTIME_SPEED_INTERVAL"); const unsigned long long parsed = text != nullptr ? std::strtoull(text, nullptr, 10) : 0ull; return parsed == 0ull ? std::uint64_t{120u} : static_cast(parsed); }(); static std::chrono::steady_clock::time_point host_start{}; static std::uint64_t guest_start{}; static std::uint64_t vblank_start{}; static bool started = false; const auto now = std::chrono::steady_clock::now(); if (!started) { host_start = now; guest_start = virtual_time_us; vblank_start = vblank_index; started = true; return; } const std::uint64_t vblanks = vblank_index - vblank_start; if (vblanks < interval) return; const double host_us = static_cast(std::max(1, std::chrono::duration_cast(now - host_start).count())); const double guest_us = static_cast(virtual_time_us - guest_start); std::ostringstream line; line << std::fixed << std::setprecision(1) << "[realtime-speed] vblank=" << vblank_index << " host_ms_per_vblank=" << host_us / static_cast(vblanks) / 1000.0 << " emulation_speed_percent=" << guest_us * 100.0 / host_us << " ge_list_ms=" << static_cast(g_speed_ge_list_ns) / 1e6 / static_cast(vblanks) << " gpu_finish_ms=" << static_cast(g_speed_gpu_finish_ns) / 1e6 / static_cast(vblanks) << " throttle_ms=" << static_cast(g_speed_throttle_ns) / 1e6 / static_cast(vblanks) << " ge_wait_ms=" << static_cast(g_speed_ge_wait_ns) / 1e6 / static_cast(vblanks) << "\n"; note_thread_switch(); std::vector> threads(g_speed_thread_ns.begin(), g_speed_thread_ns.end()); std::sort(threads.begin(), threads.end(), [](const auto &a, const auto &b) { return a.second > b.second; }); line << " threads:"; for (std::size_t i = 0; i < threads.size() && i < 6u; ++i) { const auto found = thread_table.threads.find(threads[i].first); const std::string name = found != thread_table.threads.end() ? found->second.name : "?"; line << " [" << threads[i].first << " " << name << " " << static_cast(threads[i].second) / 1000.0 * 100.0 / host_us << "%]"; } line << "\n"; g_speed_thread_ns.clear(); static std::unordered_map previous_hle; const auto hle = runtime.hle_histogram(); if (!hle.empty()) { std::vector> delta; for (const auto &[key, count] : hle) { const std::uint64_t before = previous_hle[key]; if (count > before) delta.emplace_back(key, count - before); previous_hle[key] = count; } std::sort(delta.begin(), delta.end(), [](const auto &a, const auto &b) { return a.second > b.second; }); line << " hle_per_vblank:"; for (std::size_t i = 0; i < delta.size() && i < 6u; ++i) line << " [" << delta[i].first << " " << static_cast(delta[i].second) / static_cast(vblanks) << "]"; line << "\n"; } std::cerr << line.str(); g_speed_ge_list_ns = 0u; g_speed_gpu_finish_ns = 0u; g_speed_throttle_ns = 0u; g_speed_ge_wait_ns = 0u; host_start = now; guest_start = virtual_time_us; vblank_start = vblank_index; } void check_wall_clock_limit(psprecomp::Runtime &runtime, std::uint64_t sample_mask) { if (wall_clock_limit_seconds <= 0.0 || runtime.stopped()) return; static std::uint64_t wall_clock_samples = 0u; if ((++wall_clock_samples & sample_mask) != 0u) return; const std::chrono::duration elapsed = std::chrono::steady_clock::now() - wall_clock_start; if (elapsed.count() < wall_clock_limit_seconds) return; runtime.stop("Wall-clock limit reached at " + psprecomp::hex32(runtime.cpu().pc)); } void lcs_starvation_tick(psprecomp::Runtime &starvation_runtime, psprecomp::AllegrexContext &ctx) { virtual_time_us += starvation_tick_microseconds; promote_expired_delays(); check_wall_clock_limit(starvation_runtime, 0xFFFu); if (mpeg_read_thread_args != 0u && std::getenv("LCS_MPEG_FINISH") != nullptr) { auto &memory = starvation_runtime.memory(); if (memory.contains(mpeg_read_thread_args + 0xCu, 4u)) { const std::uint32_t status_pointer = memory.load32(mpeg_read_thread_args + 0xCu); if (status_pointer != 0u && memory.contains(status_pointer, 4u) && memory.load32(status_pointer) != 0xFFu) { memory.store32(status_pointer, 0xFFu); std::cerr << "[mpeg] marked video finished at " << psprecomp::hex32(status_pointer) << "\n"; } } } if (std::getenv("LCS_MAIN_DIAG") != nullptr) { static std::uint64_t samples = 0u; if ((++samples % 20000u) == 0u) { const auto main_thread = thread_table.threads.find(3); if (main_thread != thread_table.threads.end()) { std::cerr << "[main] t=" << virtual_time_us << " state=" << static_cast(main_thread->second.state) << " delay_until=" << main_thread->second.delay_until_us << " pc=" << psprecomp::hex32(main_thread->second.suspended_context.pc) << " current=" << thread_table.current_uid << " ready=" << thread_table.continuations.size(); auto &memory = starvation_runtime.memory(); if (memory.contains(0x08B5D16Cu, 4u)) { const std::uint32_t manager = memory.load32(0x08B5D16Cu); std::cerr << " mgr=" << psprecomp::hex32(manager); if (manager != 0u && memory.contains(manager + 0xD04u, 4u)) { const std::uint32_t head = memory.load32(manager + 0xCFCu); std::cerr << " queue_head=" << psprecomp::hex32(head) << " sentinel=" << psprecomp::hex32(manager + 0xCFCu) << " empty=" << (head == manager + 0xCFCu ? 1 : 0) << " busy=" << psprecomp::hex32(memory.load32(manager + 0xD04u)); } } std::cerr << "\n"; } } } const auto current = thread_table.threads.find(thread_table.current_uid); if (current == thread_table.threads.end() || current->second.state != ThreadState::Running) return; const auto best = best_ready_thread(); if (best == thread_table.continuations.end()) return; if (thread_priority(best->uid) >= thread_priority(thread_table.current_uid)) return; enqueue_continuation(thread_table.current_uid, ctx); (void)activate_next_thread(ctx, "timer-preempt"); } std::unordered_map pc_profile; std::unordered_map pc_profile_callers; std::uint32_t g_hang_trace_lines = 0u; bool hang_trace_active() { return g_hang_trace_lines != 0u && g_hang_trace_lines < 600u; } void hang_trace(const std::string &line) { if (!hang_trace_active()) return; ++g_hang_trace_lines; std::cerr << "[hang] t=" << virtual_time_us << " vblank=" << display_vblank_index << " uid=" << thread_table.current_uid << " " << line << "\n"; } void reset_pc_profile_on_key() { if (pc_profile.empty() && pc_profile_callers.empty()) return; if (!display_window_profile_key_pressed()) return; pc_profile.clear(); pc_profile_callers.clear(); g_hang_trace_lines = 1u; std::cerr << "[lcs-profile] reset at vblank " << display_vblank_index << "\n"; } std::vector parse_address_list(const char *text) { std::vector result; if (text == nullptr) return result; std::stringstream stream(text); std::string item; while (std::getline(stream, item, ',')) { if (item.empty()) continue; result.push_back(static_cast(std::strtoul(item.c_str(), nullptr, 0))); } return result; } void lcs_pc_profile_hook(psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx, std::uint32_t target_pc, std::uint32_t) { ++pc_profile[target_pc]; ++pc_profile_callers[(static_cast(target_pc) << 32u) | ctx.gpr[31]]; static const std::vector traced = parse_address_list(std::getenv("LCS_PC_TRACE")); if (std::find(traced.begin(), traced.end(), target_pc) == traced.end()) return; std::cerr << "[lcs-trace] " << psprecomp::hex32(target_pc) << " uid=" << thread_table.current_uid << " t=" << virtual_time_us << " ra=" << psprecomp::hex32(ctx.gpr[31]) << " s4=" << psprecomp::hex32(ctx.gpr[20]) << " s5=" << psprecomp::hex32(ctx.gpr[21]) << " s6=" << psprecomp::hex32(ctx.gpr[22]); for (const std::uint32_t address : parse_address_list(std::getenv("LCS_MEM_WATCH"))) { if (!rt.memory().contains(address, 4u)) continue; std::cerr << " [" << psprecomp::hex32(address) << "]=" << static_cast(rt.memory().load32(address)); } std::cerr << "\n"; } } void set_wall_clock_limit(double seconds) { wall_clock_limit_seconds = seconds; wall_clock_start = std::chrono::steady_clock::now(); } void ge_worker_shutdown() { ge_worker_stop(); } void dump_disc_read_stats() { const std::uint64_t total = disc_read_stats.bytes_from_files + disc_read_stats.bytes_zero_filled; if (total == 0u) return; std::cerr << "[disc-read] from_files=" << disc_read_stats.bytes_from_files << " zero_filled=" << disc_read_stats.bytes_zero_filled << " zero_fill_events=" << disc_read_stats.zero_fill_events << " short_reads=" << disc_read_stats.short_reads << " zero_percent=" << (static_cast(disc_read_stats.bytes_zero_filled) * 100.0 / static_cast(total)) << " registered_files=" << file_table.virtual_files_by_path.size() << "\n"; } void dump_watched_memory(psprecomp::Runtime &runtime) { const char *text = std::getenv("LCS_MEM_WATCH"); if (text == nullptr || *text == '\0') return; std::stringstream stream(text); std::string item; while (std::getline(stream, item, ',')) { if (item.empty()) continue; const auto address = static_cast(std::strtoul(item.c_str(), nullptr, 0)); if (!runtime.memory().contains(address, 4u)) { std::cerr << "[lcs-mem] " << psprecomp::hex32(address) << " unmapped\n"; continue; } const std::uint32_t value = runtime.memory().load32(address); std::cerr << "[lcs-mem] " << psprecomp::hex32(address) << " = " << psprecomp::hex32(value) << " (" << static_cast(value) << ")\n"; } } void dump_framebuffer_stats(psprecomp::Runtime &runtime) { for (const std::uint32_t probe : {0x04000000u, 0x04088000u, 0x04178000u}) { std::uint64_t probe_non_black = 0u; std::map probe_colors; for (std::uint32_t y = 0; y < 272u; ++y) { for (std::uint32_t x = 0; x < 480u; ++x) { const std::uint32_t address = probe + (y * 512u + x) * 4u; if (!runtime.memory().contains(address, 4u)) continue; const std::uint32_t pixel = runtime.memory().load32(address) & 0x00FFFFFFu; if (pixel != 0u) ++probe_non_black; ++probe_colors[pixel]; } } std::cerr << "[lcs-fb-probe] " << psprecomp::hex32(probe) << " non_black=" << probe_non_black << " distinct=" << probe_colors.size(); std::vector> top(probe_colors.begin(), probe_colors.end()); std::sort(top.begin(), top.end(), [](const auto &a, const auto &b) { return a.second > b.second; }); for (std::size_t i = 0; i < top.size() && i < 3u; ++i) { std::cerr << " | " << psprecomp::hex32(top[i].first) << "x" << top[i].second; } std::cerr << "\n"; } const std::uint32_t base = display_state.frame_buffer; if (base == 0u) { std::cerr << "[lcs-fb] no framebuffer set\n"; return; } const std::uint32_t width = 480u; const std::uint32_t height = 272u; const std::uint32_t stride = display_state.buffer_width != 0u ? display_state.buffer_width : width; std::uint64_t non_black = 0u; std::map histogram; for (std::uint32_t y = 0; y < height; ++y) { for (std::uint32_t x = 0; x < width; ++x) { const std::uint32_t address = base + (y * stride + x) * 4u; if (!runtime.memory().contains(address, 4u)) continue; const std::uint32_t pixel = runtime.memory().load32(address) & 0x00FFFFFFu; if (pixel != 0u) ++non_black; ++histogram[pixel]; } } std::cerr << "[lcs-fb] base=" << psprecomp::hex32(base) << " stride=" << stride << " non_black=" << non_black << " distinct_colors=" << histogram.size() << "\n"; std::vector> sorted(histogram.begin(), histogram.end()); std::sort(sorted.begin(), sorted.end(), [](const auto &a, const auto &b) { return a.second > b.second; }); for (std::size_t i = 0; i < sorted.size() && i < 6u; ++i) { std::cerr << "[lcs-fb] color=" << psprecomp::hex32(sorted[i].first) << " pixels=" << sorted[i].second << "\n"; } } void dump_pc_profile() { if (pc_profile.empty()) return; std::vector> sorted(pc_profile.begin(), pc_profile.end()); std::sort(sorted.begin(), sorted.end(), [](const auto &a, const auto &b) { return a.second > b.second; }); std::cerr << "[lcs-profile] distinct_targets=" << sorted.size() << "\n"; std::vector watch_list{0x08AC72D8u, 0x08AC7860u, 0x08B0B440u, 0x08AA3190u}; if (const char *text = std::getenv("LCS_PC_WATCH")) { watch_list.clear(); std::stringstream stream(text); std::string item; while (std::getline(stream, item, ',')) { if (item.empty()) continue; watch_list.push_back(static_cast(std::strtoul(item.c_str(), nullptr, 0))); } } for (const std::uint32_t watched : watch_list) { const auto found = pc_profile.find(watched); std::cerr << "[lcs-profile] watched " << psprecomp::hex32(watched) << " calls=" << (found != pc_profile.end() ? found->second : 0u) << "\n"; } for (std::size_t i = 0; i < sorted.size() && i < 80u; ++i) { std::cerr << "[lcs-profile] " << psprecomp::hex32(sorted[i].first) << " calls=" << sorted[i].second << "\n"; } std::vector> callers(pc_profile_callers.begin(), pc_profile_callers.end()); std::sort(callers.begin(), callers.end(), [](const auto &a, const auto &b) { return a.second > b.second; }); for (std::size_t i = 0; i < callers.size() && i < 200u; ++i) { std::cerr << "[lcs-profile-caller] target=" << psprecomp::hex32(static_cast(callers[i].first >> 32u)) << " ra=" << psprecomp::hex32(static_cast(callers[i].first)) << " calls=" << callers[i].second << "\n"; } } void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start) { thread_table = ThreadTable{}; ThreadRecord module_thread{}; module_thread.name = "module_start"; module_thread.priority = 32u; module_thread.stack_size = 0x10000u; module_thread.stack_top = 0x0A000000u; module_thread.stack_bottom = module_thread.stack_top - module_thread.stack_size; module_thread.kernel_context = module_thread.stack_top - 0x100u; thread_table.next_stack_top = module_thread.stack_bottom; module_thread.state = ThreadState::Running; if (!runtime.memory().contains(module_thread.stack_bottom, module_thread.stack_size)) throw psprecomp::Error("LCS module_start stack falls outside PSP user RAM"); runtime.memory().zero(module_thread.stack_bottom, module_thread.stack_size); runtime.memory().store32(module_thread.stack_bottom, 0u); runtime.memory().store32(module_thread.kernel_context + 0xC0u, 0u); runtime.memory().store32(module_thread.kernel_context + 0xC8u, module_thread.stack_bottom); runtime.memory().store32(module_thread.kernel_context + 0xF8u, 0xFFFFFFFFu); runtime.memory().store32(module_thread.kernel_context + 0xFCu, 0xFFFFFFFFu); runtime.cpu().set_gpr(26, module_thread.kernel_context); runtime.cpu().set_gpr(29, module_thread.kernel_context); thread_table.threads.emplace(0, std::move(module_thread)); partition_table = PartitionTable{}; partition_table.next_address = (user_arena_start + 0xFFu) & ~0xFFu; callback_table = CallbackTable{}; semaphore_table = SemaphoreTable{}; event_flag_table = EventFlagTable{}; fixed_pool_table = FixedPoolTable{}; file_table = FileTable{}; loaded_modules.clear(); next_module_uid = 0x400; volatile_memory_locked = false; umd_activated = false; umd_callback_notified = false; general_purpose_io = 0u; savedata_utility = SavedataUtilityState{}; ge_worker_stop(); pending_ge_callbacks.clear(); mpeg_contexts.clear(); for (auto &state : atrac_contexts) close_atrac_decoder(state); atrac_contexts = {}; sub_interrupts.clear(); display_state = DisplayState{}; display_vblank_index = 0u; ge_callback_table = GeCallbackTable{}; ge_next_list_id = 1; async_return_frames.clear(); virtual_time_us = 0u; g_vblank_interrupt_due_us = 0u; if (std::getenv("LCS_PC_PROFILE") != nullptr) { psprecomp::set_runtime_pre_chained_call_hook(&lcs_pc_profile_hook); } { const char *interval_text = std::getenv("PSPRECOMP_TIME_TICK_DISPATCHES"); std::uint64_t interval = 256u; if (interval_text != nullptr && *interval_text != '\0') { interval = std::strtoull(interval_text, nullptr, 0); } starvation_tick_microseconds = std::max(1u, interval / 4u); psprecomp::set_runtime_starvation_hook( interval == 0u ? nullptr : &lcs_starvation_tick, interval); } runtime.register_function(0x00000000u, &lcs_module_thread_return, "psp_thread_return"); runtime.register_function(0x00000004u, &lcs_callback_return, "psp_callback_return"); std::uint32_t compiled_sdk_version = 0u; (void)compiled_sdk_version; runtime.register_hle("SysMemUserForUser", 0x7591C7DBu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("SysMemUserForUser", 0xF77D77CBu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("SysMemUserForUser", 0xA291F107u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t low = (partition_table.next_address + 0xFFu) & ~0xFFu; const std::uint32_t high = thread_table.next_stack_top & ~0xFFu; ctx.set_gpr(2, high > low ? high - low : 0u); }); runtime.register_hle("SysMemUserForUser", 0x237DBD4Fu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::string name = ctx.gpr[5] != 0u ? rt.memory().read_c_string(ctx.gpr[5], 128u) : "partition"; const std::uint32_t size = ctx.gpr[7]; const std::uint32_t alignment = 0x100u; const std::uint32_t aligned_size = (size + alignment - 1u) & ~(alignment - 1u); const std::uint32_t address = (partition_table.next_address + alignment - 1u) & ~(alignment - 1u); if (aligned_size == 0u || !rt.memory().contains(address, aligned_size)) { ctx.set_gpr(2, 0x80020190u); return; } rt.memory().zero(address, aligned_size); const std::int32_t uid = partition_table.next_uid++; partition_table.blocks.emplace(uid, PartitionBlock{name, address, aligned_size}); partition_table.next_address = address + aligned_size; ctx.set_gpr(2, static_cast(uid)); }); runtime.register_hle("SysMemUserForUser", 0x9D9A5BA1u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto it = partition_table.blocks.find(uid); ctx.set_gpr(2, it == partition_table.blocks.end() ? 0u : it->second.address); }); runtime.register_hle("SysMemUserForUser", 0xB6D61D02u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); ctx.set_gpr(2, partition_table.blocks.erase(uid) == 1u ? 0u : 0x800200CBu); }); runtime.register_hle("ThreadManForUser", 0x446D8DE6u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::string name = ctx.gpr[4] != 0u ? rt.memory().read_c_string(ctx.gpr[4], 128u) : "unnamed"; const std::uint32_t requested_stack = ctx.gpr[7]; if (requested_stack < 0x200u) { ctx.set_gpr(2, 0x80020194u); return; } const std::uint32_t stack_size = (requested_stack + 0xFFu) & ~0xFFu; std::uint32_t stack_bottom = 0u; std::uint32_t stack_top = 0u; if (!allocate_thread_stack(stack_size, stack_bottom, stack_top) || !rt.memory().contains(stack_bottom, stack_size)) { std::cerr << "[thread] create \"" << name << "\" failed: no stack space for " << stack_size << " bytes (threads=" << thread_table.threads.size() << " free_blocks=" << thread_table.free_stacks.size() << ")\n"; ctx.set_gpr(2, 0x80020190u); return; } ThreadRecord record{ name, ctx.gpr[5], ctx.gpr[6], stack_size, ctx.gpr[8], }; const std::int32_t uid = thread_table.next_uid++; record.stack_top = stack_top; record.stack_bottom = stack_bottom; record.kernel_context = stack_top - 0x100u; rt.memory().zero(stack_bottom, stack_size); rt.memory().store32(stack_bottom, static_cast(uid)); rt.memory().store32(record.kernel_context + 0xC0u, static_cast(uid)); rt.memory().store32(record.kernel_context + 0xC8u, stack_bottom); rt.memory().store32(record.kernel_context + 0xF8u, 0xFFFFFFFFu); rt.memory().store32(record.kernel_context + 0xFCu, 0xFFFFFFFFu); thread_table.threads.emplace(uid, std::move(record)); ctx.set_gpr(2, static_cast(uid)); }); runtime.register_hle("ThreadManForUser", 0xF475845Du, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto it = thread_table.threads.find(uid); if (it == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return; } ThreadRecord &thread = it->second; if (thread.state != ThreadState::Created) { ctx.set_gpr(2, 0x800201A4u); return; } const std::uint32_t arg_size = ctx.gpr[5]; const std::uint32_t arg_ptr = ctx.gpr[6]; std::uint32_t sp = thread.kernel_context; psprecomp::AllegrexContext next{}; if (arg_ptr != 0u && arg_size != 0u) { const std::uint32_t aligned_args = (arg_size + 0xFu) & ~0xFu; if (sp < thread.stack_bottom + aligned_args + 64u || !rt.memory().contains(arg_ptr, arg_size)) { ctx.set_gpr(2, 0x800200D3u); return; } sp -= aligned_args; std::vector arguments(arg_size); rt.memory().copy_out(arg_ptr, arguments); rt.memory().copy_in(sp, arguments); next.set_gpr(4, arg_size); next.set_gpr(5, sp); } else { next.set_gpr(4, 0u); next.set_gpr(5, 0u); } if (std::getenv("LCS_THREAD_DIAG") != nullptr) { std::cerr << "[thread] start \"" << thread.name << "\" arg_size=" << arg_size << " arg_ptr=" << psprecomp::hex32(arg_ptr) << " sp=" << psprecomp::hex32(sp) << "\n"; } if (thread.name == "MPEGreadThread" && arg_ptr != 0u && arg_size != 0u) { mpeg_read_thread_args = sp; } sp -= 64u; next.set_gpr(26, thread.kernel_context); next.set_gpr(28, ctx.gpr[28]); next.set_gpr(29, sp); next.set_gpr(30, sp); next.set_gpr(31, 0u); next.pc = thread.entry; enqueue_continuation(uid, next); const std::int32_t caller_uid = thread_table.current_uid; const std::uint32_t caller_priority = thread_priority(caller_uid); if (thread.priority < caller_priority) { psprecomp::AllegrexContext caller = ctx; caller.set_gpr(2, 0u); caller.pc = ctx.gpr[31]; enqueue_continuation(caller_uid, caller); (void)activate_next_thread(ctx, "thread-control"); } else { set_success(ctx); } }); runtime.register_hle("ThreadManForUser", 0x809CE29Bu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::int32_t uid = thread_table.current_uid; complete_current_thread(rt, ctx); static const bool keep = std::getenv("LCS_EXIT_DELETE_KEEP") != nullptr; if (keep || uid == 0 || uid == thread_table.current_uid) return; const auto found = thread_table.threads.find(uid); if (found == thread_table.threads.end()) return; remove_thread_from_wait_queues(uid); async_return_frames.erase(uid); release_thread_stack(found->second); thread_table.threads.erase(found); }); runtime.register_hle("ThreadManForUser", 0x383F7BCCu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::int32_t uid = static_cast(ctx.gpr[4]); if (uid == 0 || uid == thread_table.current_uid) { ctx.set_gpr(2, 0x80020197u); return; } const auto found = thread_table.threads.find(uid); if (found == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return; } const bool was_active = found->second.state != ThreadState::Created && found->second.state != ThreadState::Completed; thread_table.continuations.erase( std::remove_if(thread_table.continuations.begin(), thread_table.continuations.end(), [uid](const ThreadContinuation &item) { return item.uid == uid; }), thread_table.continuations.end()); async_return_frames.erase(uid); if (was_active) wake_thread_end_waiters(uid, 0x800201ACu); else wake_thread_end_waiters(uid, 0u); remove_thread_from_wait_queues(uid); release_thread_stack(found->second); thread_table.threads.erase(found); set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0x9FA03CD3u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::int32_t uid = static_cast(ctx.gpr[4]); if (uid == 0 || uid == thread_table.current_uid) { ctx.set_gpr(2, 0x800201A4u); return; } const auto found = thread_table.threads.find(uid); if (found == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return; } if (found->second.state != ThreadState::Created && found->second.state != ThreadState::Completed) { ctx.set_gpr(2, 0x800201A4u); return; } remove_thread_from_wait_queues(uid); async_return_frames.erase(uid); release_thread_stack(found->second); thread_table.threads.erase(found); set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0x9944F31Fu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::int32_t uid = static_cast(ctx.gpr[4]); if (uid == 0 || uid == thread_table.current_uid) { ctx.set_gpr(2, 0x80020197u); return; } const auto found = thread_table.threads.find(uid); if (found == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return; } ThreadRecord &thread = found->second; if (thread.state == ThreadState::Completed || thread.state == ThreadState::Created) { ctx.set_gpr(2, 0x800201A2u); return; } if (thread.externally_suspended) { ctx.set_gpr(2, 0x800201A3u); return; } thread.externally_suspended = true; const auto continuation = std::find_if( thread_table.continuations.begin(), thread_table.continuations.end(), [uid](const ThreadContinuation &item) { return item.uid == uid; }); if (continuation != thread_table.continuations.end()) { thread.suspended_context = continuation->context; thread.state = ThreadState::Ready; thread_table.continuations.erase(continuation); } set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0x75156E8Fu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::int32_t uid = static_cast(ctx.gpr[4]); if (uid == 0 || uid == thread_table.current_uid) { ctx.set_gpr(2, 0x80020197u); return; } const auto found = thread_table.threads.find(uid); if (found == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return; } ThreadRecord &thread = found->second; if (!thread.externally_suspended) { ctx.set_gpr(2, 0x800201A5u); return; } thread.externally_suspended = false; if (thread.state == ThreadState::Ready) enqueue_continuation(uid, thread.suspended_context); set_success(ctx); (void)preempt_if_higher_priority(ctx, "thread-resume"); }); runtime.register_hle("ThreadManForUser", 0x293B45B8u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, static_cast(thread_table.current_uid)); }); runtime.register_hle("ThreadManForUser", 0x71BC9871u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { std::int32_t uid = static_cast(ctx.gpr[4]); if (uid == 0) uid = thread_table.current_uid; std::uint32_t priority = ctx.gpr[5]; if (priority == 0u) priority = thread_priority(thread_table.current_uid); const auto found = thread_table.threads.find(uid); if (found == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return; } ThreadRecord &thread = found->second; if (thread.state == ThreadState::Created || thread.state == ThreadState::Completed) { ctx.set_gpr(2, 0x800201A2u); return; } if (priority < 0x08u || priority > 0x77u) { ctx.set_gpr(2, 0x80020193u); return; } thread.priority = priority; const auto best = best_ready_thread(); const std::uint32_t current_priority = thread_priority(thread_table.current_uid); if (best != thread_table.continuations.end() && thread_priority(best->uid) < current_priority) { const std::int32_t caller_uid = thread_table.current_uid; psprecomp::AllegrexContext caller = ctx; caller.set_gpr(2, 0u); caller.pc = ctx.gpr[31]; enqueue_continuation(caller_uid, caller); (void)activate_next_thread(ctx, "thread-control"); return; } set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0x110DEC9Au, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t output = ctx.gpr[5]; if (!rt.memory().contains(output, 8u)) { ctx.set_gpr(2, 0x800200D3u); return; } rt.memory().store32(output, ctx.gpr[4]); rt.memory().store32(output + 4u, 0u); set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0xC8CD158Cu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, ctx.gpr[4]); ctx.set_gpr(3, 0u); }); runtime.register_hle("ThreadManForUser", 0xBA6B92E2u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t clock = ctx.gpr[4]; const std::uint32_t seconds_out = ctx.gpr[5]; const std::uint32_t usec_out = ctx.gpr[6]; if (!rt.memory().contains(clock, 8u)) { ctx.set_gpr(2, 0x800200D3u); return; } const std::uint64_t ticks = static_cast(rt.memory().load32(clock)) | (static_cast(rt.memory().load32(clock + 4u)) << 32u); if (rt.memory().contains(seconds_out, 4u)) rt.memory().store32(seconds_out, static_cast(ticks / 1'000'000u)); if (rt.memory().contains(usec_out, 4u)) rt.memory().store32(usec_out, static_cast(ticks % 1'000'000u)); set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0xE1619D7Cu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint64_t ticks = static_cast(ctx.gpr[4]) | (static_cast(ctx.gpr[5]) << 32u); if (rt.memory().contains(ctx.gpr[6], 4u)) rt.memory().store32(ctx.gpr[6], static_cast(ticks / 1'000'000u)); if (rt.memory().contains(ctx.gpr[7], 4u)) rt.memory().store32(ctx.gpr[7], static_cast(ticks % 1'000'000u)); set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0xDB738F35u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t output = ctx.gpr[4]; if (!rt.memory().contains(output, 8u)) { ctx.set_gpr(2, 0x800200D3u); return; } const std::uint64_t usec = system_time_microseconds(); rt.memory().store32(output, static_cast(usec)); rt.memory().store32(output + 4u, static_cast(usec >> 32u)); set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0x82BC5777u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint64_t usec = system_time_microseconds(); ctx.set_gpr(2, static_cast(usec)); ctx.set_gpr(3, static_cast(usec >> 32u)); }); runtime.register_hle("ThreadManForUser", 0x369ED59Du, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, static_cast(system_time_microseconds())); }); const auto refer_profiler = [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, 0u); }; runtime.register_hle("ThreadManForUser", 0x64D4540Eu, refer_profiler); runtime.register_hle("ThreadManForUser", 0x8218B4DDu, refer_profiler); runtime.register_hle("ThreadManForUser", 0xEA748E31u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t reserved = ctx.gpr[4]; const std::uint32_t attributes = ctx.gpr[5]; if (reserved != 0u) { ctx.set_gpr(2, 0x800200D2u); return; } if (auto current = thread_table.threads.find(thread_table.current_uid); current != thread_table.threads.end()) { current->second.attributes |= attributes; } set_success(ctx); }); auto sleep_thread = [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { (void)sleep_current_thread(rt, ctx); }; runtime.register_hle("ThreadManForUser", 0x9ACE131Eu, sleep_thread); runtime.register_hle("ThreadManForUser", 0x82826F70u, sleep_thread); runtime.register_hle("ThreadManForUser", 0xD59EAD2Fu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t result = wake_thread(static_cast(ctx.gpr[4])); ctx.set_gpr(2, result); if (result == 0u) (void)preempt_if_higher_priority(ctx, "thread-wakeup"); }); runtime.register_hle("ThreadManForUser", 0xFCCFAD26u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto found = thread_table.threads.find(static_cast(ctx.gpr[4])); if (found == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return; } const std::uint32_t previous = found->second.wakeup_count; found->second.wakeup_count = 0u; ctx.set_gpr(2, previous); }); runtime.register_hle("ThreadManForUser", 0xAA73C935u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { if (auto current = thread_table.threads.find(thread_table.current_uid); current != thread_table.threads.end()) { current->second.exit_status = ctx.gpr[4]; } complete_current_thread(rt, ctx); }); runtime.register_hle("ThreadManForUser", 0x278C0DF5u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto it = thread_table.threads.find(uid); if (uid <= 0 || it == thread_table.threads.end()) { ctx.set_gpr(2, 0x80020198u); return; } if (it->second.state == ThreadState::Completed) { set_success(ctx); return; } psprecomp::AllegrexContext waiter = ctx; waiter.set_gpr(2, 0u); waiter.pc = ctx.gpr[31]; thread_table.thread_end_waiters[uid].push_back({thread_table.current_uid, waiter}); if (auto current = thread_table.threads.find(thread_table.current_uid); current != thread_table.threads.end()) { current->second.state = ThreadState::Sleeping; current->second.suspended_context = waiter; } if (!activate_next_thread(ctx, "kernel-wait")) { rt.stop("PSP thread wait deadlock on uid " + std::to_string(uid)); } }); auto delay_thread = [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { if (thread_table.current_uid == 3) hang_trace("delay us=" + std::to_string(ctx.gpr[4]) + " ra=" + psprecomp::hex32(ctx.gpr[31])); if (deliver_pending_ge_callback(ctx, ctx.gpr[4])) return; if (dispatch_vblank_interrupt_if_due(rt, ctx, ctx.gpr[4])) return; (void)delay_current_thread(rt, ctx, ctx.gpr[4]); }; auto delay_thread_cb = [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { if (thread_table.current_uid == 3) hang_trace("delaycb us=" + std::to_string(ctx.gpr[4]) + " ra=" + psprecomp::hex32(ctx.gpr[31])); if (try_dispatch_pending_callback(ctx)) return; if (deliver_pending_ge_callback(ctx, ctx.gpr[4])) return; if (dispatch_vblank_interrupt_if_due(rt, ctx, ctx.gpr[4])) return; (void)delay_current_thread(rt, ctx, ctx.gpr[4]); }; runtime.register_hle("ThreadManForUser", 0xCEADEB47u, delay_thread); runtime.register_hle("ThreadManForUser", 0x68DA9E36u, delay_thread_cb); runtime.register_hle("ThreadManForUser", 0xE81CAF8Fu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::string name = ctx.gpr[4] != 0u ? rt.memory().read_c_string(ctx.gpr[4], 128u) : "callback"; const std::int32_t uid = callback_table.next_uid++; if (std::getenv("LCS_CB_DIAG") != nullptr) { std::cerr << "[cb] CreateCallback uid=" << uid << " name=\"" << name << "\" func=" << psprecomp::hex32(ctx.gpr[5]) << " common=" << psprecomp::hex32(ctx.gpr[6]) << "\n"; } callback_table.callbacks.emplace(uid, CallbackRecord{ name, ctx.gpr[5], ctx.gpr[6], thread_table.current_uid, 0u, 0u}); notify_umd_callback(); ctx.set_gpr(2, static_cast(uid)); }); runtime.register_hle("ThreadManForUser", 0xEDBA5844u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); ctx.set_gpr(2, callback_table.callbacks.erase(uid) == 1u ? 0u : 0x800201A1u); }); runtime.register_hle("ThreadManForUser", 0x349D6D6Cu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { virtual_time_us += 25u; promote_expired_delays(); if (!try_dispatch_pending_callback(ctx)) { set_success(ctx); (void)preempt_if_higher_priority(ctx, "check-callback"); } }); runtime.register_hle("ThreadManForUser", 0xD6DA4BA1u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::string name = ctx.gpr[4] != 0u ? rt.memory().read_c_string(ctx.gpr[4], 128u) : "semaphore"; const auto initial = static_cast(ctx.gpr[6]); const auto maximum = static_cast(ctx.gpr[7]); if (initial < 0 || maximum <= 0 || initial > maximum) { ctx.set_gpr(2, 0x800201B0u); return; } const std::int32_t uid = semaphore_table.next_uid++; semaphore_table.semaphores.emplace(uid, SemaphoreRecord{name, initial, maximum, {}}); ctx.set_gpr(2, static_cast(uid)); }); runtime.register_hle("ThreadManForUser", 0x28B6489Cu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto found = semaphore_table.semaphores.find(uid); if (found == semaphore_table.semaphores.end()) { ctx.set_gpr(2, 0x80020199u); return; } for (auto &waiter : found->second.waiters) { waiter.context.set_gpr(2, 0x800201A7u); enqueue_continuation(waiter.uid, waiter.context); } semaphore_table.semaphores.erase(found); set_success(ctx); (void)preempt_if_higher_priority(ctx, "semaphore-delete"); }); runtime.register_hle("ThreadManForUser", 0x3F53E640u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto amount = static_cast(ctx.gpr[5]); const auto it = semaphore_table.semaphores.find(uid); if (it == semaphore_table.semaphores.end() || amount <= 0 || static_cast(it->second.count) + amount > it->second.maximum) { ctx.set_gpr(2, 0x80020199u); return; } SemaphoreRecord &semaphore = it->second; if (const char *watched = std::getenv("LCS_SEMA_DIAG"); watched != nullptr && (semaphore.name == watched || std::string(watched) == "ALL")) { std::cerr << "[sema] SIGNAL \"" << semaphore.name << "\" +" << amount << " count=" << semaphore.count << " waiters=" << semaphore.waiters.size() << " uid=" << thread_table.current_uid << " ra=" << psprecomp::hex32(ctx.gpr[31]) << "\n"; } semaphore.count += amount; auto waiter = semaphore.waiters.begin(); while (waiter != semaphore.waiters.end()) { if (semaphore.count >= waiter->requested) { semaphore.count -= waiter->requested; waiter->context.set_gpr(2, 0u); enqueue_continuation(waiter->uid, waiter->context); waiter = semaphore.waiters.erase(waiter); } else { ++waiter; } } set_success(ctx); (void)preempt_if_higher_priority(ctx, "semaphore-signal"); }); auto semaphore_wait = [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto amount = static_cast(ctx.gpr[5]); const auto it = semaphore_table.semaphores.find(uid); if (it == semaphore_table.semaphores.end() || amount <= 0 || amount > it->second.maximum) { ctx.set_gpr(2, 0x80020199u); return; } if (const char *watched = std::getenv("LCS_SEMA_DIAG"); watched != nullptr && it->second.name == watched) { std::cerr << "[sema] WAIT \"" << it->second.name << "\" -" << amount << " count=" << it->second.count << " blocks=" << (it->second.count >= amount ? 0 : 1) << " uid=" << thread_table.current_uid << " ra=" << psprecomp::hex32(ctx.gpr[31]) << "\n"; } if (it->second.count >= amount) { it->second.count -= amount; set_success(ctx); return; } const psprecomp::AllegrexContext suspended = make_wait_context(ctx); it->second.waiters.push_back(SemaphoreWaiter{thread_table.current_uid, suspended, amount}); (void)suspend_current_thread(rt, ctx, suspended, "semaphore " + std::to_string(uid)); }; runtime.register_hle("ThreadManForUser", 0x4E3A1105u, semaphore_wait); runtime.register_hle("ThreadManForUser", 0x6D212BACu, semaphore_wait); runtime.register_hle("ThreadManForUser", 0x58B1F937u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto amount = static_cast(ctx.gpr[5]); const auto it = semaphore_table.semaphores.find(uid); if (it == semaphore_table.semaphores.end() || amount <= 0 || amount > it->second.maximum) { ctx.set_gpr(2, 0x80020199u); return; } if (it->second.count < amount) { ctx.set_gpr(2, 0x800201AEu); return; } it->second.count -= amount; set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0x55C20A00u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::string name = ctx.gpr[4] != 0u ? rt.memory().read_c_string(ctx.gpr[4], 128u) : "event_flag"; const std::int32_t uid = event_flag_table.next_uid++; std::uint32_t initial_pattern = ctx.gpr[6]; if (name == "UmdStreamEventFlag") initial_pattern |= 0x1u; event_flag_table.flags.emplace(uid, EventFlagRecord{name, ctx.gpr[5], ctx.gpr[6], initial_pattern, {}}); ctx.set_gpr(2, static_cast(uid)); }); runtime.register_hle("ThreadManForUser", 0xEF9E4C70u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto found = event_flag_table.flags.find(uid); if (found == event_flag_table.flags.end()) { ctx.set_gpr(2, 0x8002019Au); return; } for (auto &waiter : found->second.waiters) { waiter.context.set_gpr(2, 0x800201A7u); enqueue_continuation(waiter.uid, waiter.context); } event_flag_table.flags.erase(found); set_success(ctx); (void)preempt_if_higher_priority(ctx, "event-flag-delete"); }); runtime.register_hle("ThreadManForUser", 0x1FB15A32u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto it = event_flag_table.flags.find(static_cast(ctx.gpr[4])); if (it == event_flag_table.flags.end()) { ctx.set_gpr(2, 0x8002019Au); return; } EventFlagRecord &flag = it->second; if (const char *watched = std::getenv("LCS_FLAG_DIAG"); watched != nullptr && flag.name == watched) { std::cerr << "[flag] SET \"" << flag.name << "\" bits=" << psprecomp::hex32(ctx.gpr[5]) << " pattern " << psprecomp::hex32(flag.current_pattern) << " -> " << psprecomp::hex32(flag.current_pattern | ctx.gpr[5]) << " uid=" << thread_table.current_uid << " ra=" << psprecomp::hex32(ctx.gpr[31]) << "\n"; } if (std::getenv("LCS_WORLD_DIAG") != nullptr && flag.name == "WorldStreamEventFlag") { std::cerr << "[world] SET bits=" << psprecomp::hex32(ctx.gpr[5]) << " pattern " << psprecomp::hex32(flag.current_pattern) << " -> " << psprecomp::hex32(flag.current_pattern | ctx.gpr[5]) << " uid=" << thread_table.current_uid << " ra=" << psprecomp::hex32(ctx.gpr[31]); if (rt.memory().contains(0x08B5D16Cu, 4u)) { const std::uint32_t manager = rt.memory().load32(0x08B5D16Cu); if (manager != 0u && rt.memory().contains(manager + 0xD04u, 4u)) { const std::uint32_t head = rt.memory().load32(manager + 0xCFCu); const std::uint32_t free_head = rt.memory().load32(manager + 0xCF4u); std::cerr << " queue_empty=" << (head == manager + 0xCFCu ? 1 : 0) << " free_empty=" << (free_head == manager + 0xCF4u ? 1 : 0) << " busy=" << psprecomp::hex32(rt.memory().load32(manager + 0xD04u)) << " msg=\"" << rt.memory().read_c_string(0x08B2C1C8u) << "\""; } } std::cerr << "\n"; } if (flag.name == "UmdStreamEventFlag" && (ctx.gpr[5] & 0x2u) != 0u && std::getenv("LCS_GATE_NUDGE") != nullptr && rt.memory().contains(0x08B56950u, 4u)) { const std::uint32_t current = rt.memory().load32(0x08B56950u); if (current < 2u) rt.memory().store32(0x08B56950u, current + 1u); if (rt.memory().contains(0x08B5691Cu, 1u)) rt.memory().store8(0x08B5691Cu, 0u); if (rt.memory().contains(0x08B5691Du, 1u)) rt.memory().store8(0x08B5691Du, 0u); if (std::getenv("LCS_GATE_NUDGE_BUSY") != nullptr && rt.memory().contains(0x08B5691Eu, 1u)) rt.memory().store8(0x08B5691Eu, 0u); if (std::getenv("LCS_STREAM_DIAG") != nullptr) { static std::uint32_t stream_events = 0u; std::cerr << "[stream] event #" << ++stream_events << " uid=" << thread_table.current_uid << " ra=" << psprecomp::hex32(ctx.gpr[31]) << " t=" << virtual_time_us << "\n"; } } flag.current_pattern |= ctx.gpr[5]; auto waiter = flag.waiters.begin(); while (waiter != flag.waiters.end()) { if (!event_flag_matches(flag, waiter->requested, waiter->mode)) { ++waiter; continue; } if (waiter->output_address != 0u && rt.memory().contains(waiter->output_address, 4u)) rt.memory().store32(waiter->output_address, flag.current_pattern); consume_event_flag(flag, waiter->requested, waiter->mode); waiter->context.set_gpr(2, 0u); enqueue_continuation(waiter->uid, waiter->context); waiter = flag.waiters.erase(waiter); } set_success(ctx); (void)preempt_if_higher_priority(ctx, "event-flag-set"); }); runtime.register_hle("ThreadManForUser", 0x812346E4u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto it = event_flag_table.flags.find(static_cast(ctx.gpr[4])); if (it == event_flag_table.flags.end()) { ctx.set_gpr(2, 0x8002019Au); return; } if (std::getenv("LCS_WORLD_DIAG") != nullptr && it->second.name == "WorldStreamEventFlag") { std::cerr << "[world] CLEAR mask=" << psprecomp::hex32(ctx.gpr[5]) << " pattern " << psprecomp::hex32(it->second.current_pattern) << " -> " << psprecomp::hex32(it->second.current_pattern & ctx.gpr[5]) << " uid=" << thread_table.current_uid << " ra=" << psprecomp::hex32(ctx.gpr[31]) << "\n"; } it->second.current_pattern &= ctx.gpr[5]; set_success(ctx); }); auto event_flag_wait = [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto it = event_flag_table.flags.find(uid); if (it == event_flag_table.flags.end()) { ctx.set_gpr(2, 0x8002019Au); return; } const std::uint32_t requested = ctx.gpr[5]; const std::uint32_t mode = ctx.gpr[6]; if (requested == 0u || (mode & ~0x31u) != 0u) { ctx.set_gpr(2, 0x800201B1u); return; } const bool matched = event_flag_matches(it->second, requested, mode); if (const char *watched = std::getenv("LCS_FLAG_DIAG"); watched != nullptr && it->second.name == watched) { std::cerr << "[flag] WAIT \"" << it->second.name << "\" pattern=" << psprecomp::hex32(it->second.current_pattern) << " req=" << psprecomp::hex32(requested) << " mode=" << psprecomp::hex32(mode) << " matched=" << (matched ? 1 : 0) << " uid=" << thread_table.current_uid << " ra=" << psprecomp::hex32(ctx.gpr[31]) << "\n"; } if (std::getenv("LCS_WAIT_DIAG") != nullptr && it->second.name == "UmdStreamEventFlag") { static std::uint64_t matched_count = 0u, blocked_count = 0u; (matched ? matched_count : blocked_count)++; if (((matched_count + blocked_count) % 2000u) == 0u) { std::cerr << "[wait] UmdStreamEventFlag matched=" << matched_count << " blocked=" << blocked_count << " pattern=" << psprecomp::hex32(it->second.current_pattern) << " req=" << psprecomp::hex32(requested) << " mode=" << psprecomp::hex32(mode) << "\n"; } } if (matched) { if (ctx.gpr[7] != 0u && rt.memory().contains(ctx.gpr[7], 4u)) rt.memory().store32(ctx.gpr[7], it->second.current_pattern); consume_event_flag(it->second, requested, mode); set_success(ctx); return; } const psprecomp::AllegrexContext suspended = make_wait_context(ctx); it->second.waiters.push_back(EventFlagWaiter{ thread_table.current_uid, suspended, requested, mode, ctx.gpr[7]}); (void)suspend_current_thread(rt, ctx, suspended, "event flag " + std::to_string(uid)); }; runtime.register_hle("ThreadManForUser", 0x402FCF22u, event_flag_wait); runtime.register_hle("ThreadManForUser", 0x328C546Au, event_flag_wait); runtime.register_hle("ThreadManForUser", 0x30FD48F0u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto it = event_flag_table.flags.find(static_cast(ctx.gpr[4])); if (it == event_flag_table.flags.end()) { ctx.set_gpr(2, 0x8002019Au); return; } const std::uint32_t requested = ctx.gpr[5]; const std::uint32_t mode = ctx.gpr[6]; if (std::getenv("LCS_POLL_DIAG") != nullptr) { static std::uint64_t polls = 0u; if ((++polls % 5000u) == 0u) { std::cerr << "[poll] #" << polls << " uid=" << thread_table.current_uid << " flag=\"" << it->second.name << "\" pattern=" << psprecomp::hex32(it->second.current_pattern) << " req=" << psprecomp::hex32(requested) << " mode=" << psprecomp::hex32(mode) << "\n"; } } if (!event_flag_matches(it->second, requested, mode)) { if (ctx.gpr[7] != 0u && rt.memory().contains(ctx.gpr[7], 4u)) rt.memory().store32(ctx.gpr[7], it->second.current_pattern); ctx.set_gpr(2, 0x800201AFu); return; } if (ctx.gpr[7] != 0u && rt.memory().contains(ctx.gpr[7], 4u)) rt.memory().store32(ctx.gpr[7], it->second.current_pattern); consume_event_flag(it->second, requested, mode); set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0xC07BB470u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::string name = ctx.gpr[4] != 0u ? rt.memory().read_c_string(ctx.gpr[4], 128u) : "fpl"; const std::uint32_t block_size = ctx.gpr[7]; const std::uint32_t block_count = ctx.gpr[8]; if (block_size == 0u || block_count == 0u || block_size > 0xFFFFFFFFu / block_count) { ctx.set_gpr(2, 0x800201B0u); return; } const std::uint32_t alignment = 0x100u; const std::uint32_t total = block_size * block_count; const std::uint32_t address = (partition_table.next_address + alignment - 1u) & ~(alignment - 1u); const std::uint32_t reserved = (total + alignment - 1u) & ~(alignment - 1u); if (!rt.memory().contains(address, reserved) || address + reserved > thread_table.next_stack_top) { ctx.set_gpr(2, 0x80020190u); return; } rt.memory().zero(address, reserved); const std::int32_t uid = fixed_pool_table.next_uid++; fixed_pool_table.pools.emplace(uid, FixedPoolRecord{name, address, block_size, block_count, std::vector(block_count, false)}); partition_table.next_address = address + reserved; ctx.set_gpr(2, static_cast(uid)); }); runtime.register_hle("ThreadManForUser", 0xD979E9BFu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const std::uint32_t output = ctx.gpr[5]; const auto it = fixed_pool_table.pools.find(uid); if (it == fixed_pool_table.pools.end() || !rt.memory().contains(output, 4u)) { ctx.set_gpr(2, 0x800201A8u); return; } auto &pool = it->second; const auto free_it = std::find(pool.allocated.begin(), pool.allocated.end(), false); if (free_it == pool.allocated.end()) { ctx.set_gpr(2, 0x80020190u); return; } const std::size_t index = static_cast(free_it - pool.allocated.begin()); pool.allocated[index] = true; rt.memory().store32(output, pool.address + static_cast(index) * pool.block_size); set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0x623AE665u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { rt.invoke_import("ThreadManForUser", 0xD979E9BFu, ctx); }); runtime.register_hle("ThreadManForUser", 0xF6414A71u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const std::uint32_t block = ctx.gpr[5]; const auto it = fixed_pool_table.pools.find(uid); if (it == fixed_pool_table.pools.end()) { ctx.set_gpr(2, 0x800201A8u); return; } auto &pool = it->second; if (block < pool.address || pool.block_size == 0u) { ctx.set_gpr(2, 0x800201A9u); return; } const std::uint32_t offset = block - pool.address; const std::size_t index = offset / pool.block_size; if (offset % pool.block_size != 0u || index >= pool.allocated.size()) { ctx.set_gpr(2, 0x800201A9u); return; } pool.allocated[index] = false; set_success(ctx); }); runtime.register_hle("ThreadManForUser", 0xED1410E0u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto it = fixed_pool_table.pools.find(uid); if (it == fixed_pool_table.pools.end()) { ctx.set_gpr(2, 0x800201A8u); return; } const auto &pool = it->second; constexpr std::uint32_t alignment = 0x100u; const std::uint32_t reserved = (pool.block_size * pool.block_count + alignment - 1u) & ~(alignment - 1u); if (pool.address + reserved == partition_table.next_address) partition_table.next_address = pool.address; fixed_pool_table.pools.erase(it); set_success(ctx); }); runtime.register_hle("IoFileMgrForUser", 0xB293727Fu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("IoFileMgrForUser", 0x54F5FB11u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::string device = ctx.gpr[4] != 0u ? rt.memory().read_c_string(ctx.gpr[4], 128u) : std::string{}; const std::uint32_t command = ctx.gpr[5]; const std::uint32_t input = ctx.gpr[6]; const std::uint32_t input_length = ctx.gpr[7]; const std::uint32_t output = rt.memory().contains(ctx.gpr[29] + 16u, 8u) ? rt.memory().load32(ctx.gpr[29] + 16u) : 0u; const std::uint32_t output_length = rt.memory().contains(ctx.gpr[29] + 20u, 4u) ? rt.memory().load32(ctx.gpr[29] + 20u) : 0u; if (command == 0x02425823u && (device == "fatms0:" || device == "ms0:")) { if (output == 0u || !rt.memory().contains(output, 4u)) { ctx.set_gpr(2, 0x80010016u); return; } rt.memory().store32(output, memory_stick_fat_state); set_success(ctx); return; } if (command == 0x02415823u && (device == "fatms0:" || device == "ms0:")) { if (input == 0u || input_length < 4u || !rt.memory().contains(input, 4u)) { ctx.set_gpr(2, 0x80010016u); return; } memory_stick_fat_state = rt.memory().load32(input) != 0u ? 1u : 0u; set_success(ctx); return; } if (command == 0x02425824u && (device == "fatms0:" || device == "ms0:")) { if (output == 0u || output_length < 4u || !rt.memory().contains(output, 4u)) { ctx.set_gpr(2, 0x80010016u); return; } rt.memory().store32(output, 0u); set_success(ctx); return; } if (command == 0x02025806u && (device == "mscmhc0:" || device == "ms0:")) { if (output == 0u || output_length < 4u || !rt.memory().contains(output, 4u)) { ctx.set_gpr(2, 0x80010016u); return; } rt.memory().store32(output, 1u); set_success(ctx); return; } if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { std::cerr << "[io] unsupported sceIoDevctl device=" << device << " cmd=0x" << std::hex << command << std::dec << "\n"; } ctx.set_gpr(2, 0x80010016u); }); runtime.register_hle("IoFileMgrForUser", 0x109F50BCu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::string path = rt.memory().read_c_string(ctx.gpr[4]); if (path.rfind("disc0:/sce_lbn0x", 0u) == 0u) { const auto size_marker = path.find("_size0x"); if (size_marker != std::string::npos) { const std::string lbn_text = path.substr(16u, size_marker - 16u); char *lbn_end = nullptr; const unsigned long long raw_lbn = std::strtoull(lbn_text.c_str(), &lbn_end, 16); const std::string size_text = path.substr(size_marker + 7u); char *size_end = nullptr; const unsigned long long declared_size = std::strtoull(size_text.c_str(), &size_end, 16); const bool parsed = lbn_end != lbn_text.c_str() && *lbn_end == '\0' && size_end != size_text.c_str() && raw_lbn <= 0xFFFFFFFFull; if (parsed) { if (const auto *disc_file = find_virtual_disc_file(static_cast(raw_lbn))) { std::fstream stream(disc_file->native_path, std::ios::binary | std::ios::in); if (stream) { const auto fd = file_table.next_fd++; if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { std::cerr << "[io] raw UMD open lbn=" << raw_lbn << " size=" << declared_size << " native=\"" << disc_file->native_path.filename().string() << "\"\n"; } file_table.files.emplace(fd, std::move(stream)); file_table.raw_sector_files.emplace( fd, RawSectorFile{0ull, disc_file->size}); ctx.set_gpr(2, static_cast(fd)); return; } } const std::uint64_t base_offset = raw_lbn * 2048ull; const std::uint64_t virtual_disc_size = static_cast(file_table.next_virtual_sector) * 2048ull; const auto fd = file_table.next_fd++; file_table.virtual_disc_handles.emplace( fd, VirtualDiscHandle{base_offset, declared_size, 0u}); if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { std::cerr << "[io] virtual UMD range fd=" << fd << " lbn=" << raw_lbn << " size=" << declared_size << " disc_size=" << virtual_disc_size << "\n"; } ctx.set_gpr(2, static_cast(fd)); return; } } } const auto native = rt.translate_path(path); const std::uint32_t flags = ctx.gpr[5]; std::ios::openmode mode = std::ios::binary; if ((flags & 0x0001u) != 0u) mode |= std::ios::in; if ((flags & 0x0002u) != 0u) mode |= std::ios::out; std::fstream stream(native, mode); if (!stream) { if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { std::cerr << "[io] sceIoOpen failed psp=\"" << path << "\" native=\"" << native.string() << "\"\n"; } ctx.set_gpr(2, 0x80010002u); return; } const auto fd = file_table.next_fd++; if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { std::cerr << "[io] sceIoOpen fd=" << fd << " psp=\"" << path << "\"\n"; } file_table.files.emplace(fd, std::move(stream)); ctx.set_gpr(2, static_cast(fd)); }); runtime.register_hle("IoFileMgrForUser", 0x810C4BC3u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto fd = static_cast(ctx.gpr[4]); file_table.raw_sector_files.erase(fd); if (file_table.virtual_disc_handles.erase(fd) == 1u) { ctx.set_gpr(2, 0u); return; } ctx.set_gpr(2, file_table.files.erase(fd) == 1u ? 0u : 0x80010009u); }); runtime.register_hle("IoFileMgrForUser", 0x6A638D83u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto fd = static_cast(ctx.gpr[4]); const std::uint32_t dst = ctx.gpr[5]; std::uint32_t size = ctx.gpr[6]; if (const auto virtual_handle = file_table.virtual_disc_handles.find(fd); virtual_handle != file_table.virtual_disc_handles.end()) { if (!rt.memory().contains(dst, size)) { ctx.set_gpr(2, 0x80010009u); return; } std::uint8_t *destination = rt.memory().raw_pointer(dst, size); if (destination == nullptr) { ctx.set_gpr(2, 0x80010009u); return; } const auto read = read_virtual_disc(virtual_handle->second, std::span(destination, size)); if (read != 0u) { (void)defer_current_thread_for_io_handoff( ctx, static_cast(read)); } else { ctx.set_gpr(2, static_cast(read)); } return; } const auto it = file_table.files.find(fd); if (it == file_table.files.end() || !rt.memory().contains(dst, size)) { ctx.set_gpr(2, 0x80010009u); return; } const auto bound = file_table.raw_sector_files.find(fd); if (bound != file_table.raw_sector_files.end()) { it->second.clear(); const auto position = static_cast(it->second.tellg()); const std::uint64_t end = bound->second.base + bound->second.size; const std::uint64_t remaining = position < end ? end - position : 0ull; if (remaining < size) size = static_cast(remaining); if (size == 0u) { ctx.set_gpr(2, 0u); return; } } std::uint8_t *guest_destination = rt.memory().raw_pointer(dst, size); if (guest_destination == nullptr) { ctx.set_gpr(2, 0x80010009u); return; } it->second.clear(); it->second.read(reinterpret_cast(guest_destination), static_cast(size)); const auto read = static_cast(it->second.gcount()); if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { std::cerr << "[io] sceIoRead fd=" << fd << " size=" << size << " read=" << read << "\n"; } ctx.set_gpr(2, read); }); runtime.register_hle("IoFileMgrForUser", 0x42EC03ACu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto fd = static_cast(ctx.gpr[4]); const std::uint32_t src = ctx.gpr[5]; const std::uint32_t size = ctx.gpr[6]; const auto it = file_table.files.find(fd); if (it == file_table.files.end() || !rt.memory().contains(src, size)) { ctx.set_gpr(2, 0x80010009u); return; } const std::uint8_t *guest_source = rt.memory().raw_pointer(src, size); if (guest_source == nullptr) { ctx.set_gpr(2, 0x80010009u); return; } it->second.clear(); it->second.write(reinterpret_cast(guest_source), static_cast(size)); ctx.set_gpr(2, it->second ? size : 0u); }); runtime.register_hle("IoFileMgrForUser", 0x27EB27B8u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto fd = static_cast(ctx.gpr[4]); const std::uint64_t raw_offset = static_cast(ctx.gpr[6]) | (static_cast(ctx.gpr[7]) << 32u); const auto offset = static_cast(raw_offset); const auto whence = static_cast(ctx.gpr[8]); if (const auto virtual_handle = file_table.virtual_disc_handles.find(fd); virtual_handle != file_table.virtual_disc_handles.end() && whence >= 0 && whence <= 2) { VirtualDiscHandle &handle = virtual_handle->second; std::int64_t target = offset; if (whence == 1) target += static_cast(handle.position); if (whence == 2) target += static_cast(handle.length); if (target < 0) target = 0; if (static_cast(target) > handle.length) target = static_cast(handle.length); handle.position = static_cast(target); ctx.set_gpr(2, static_cast(handle.position)); ctx.set_gpr(3, static_cast(handle.position >> 32u)); return; } const auto it = file_table.files.find(fd); if (it == file_table.files.end() || whence < 0 || whence > 2) { ctx.set_gpr(2, 0x80010009u); ctx.set_gpr(3, 0xFFFFFFFFu); return; } std::ios_base::seekdir direction = std::ios::beg; if (whence == 1) direction = std::ios::cur; if (whence == 2) direction = std::ios::end; it->second.clear(); const auto raw = file_table.raw_sector_files.find(fd); if (raw != file_table.raw_sector_files.end()) { std::int64_t target = offset; if (whence == 0) target += static_cast(raw->second.base); if (whence == 1) target += static_cast(it->second.tellg()); if (whence == 2) { target += static_cast(raw->second.base + raw->second.size); } it->second.seekg(static_cast(target), std::ios::beg); } else { it->second.seekg(static_cast(offset), direction); } if (!it->second) { ctx.set_gpr(2, 0x80010016u); ctx.set_gpr(3, 0xFFFFFFFFu); return; } auto position = static_cast(it->second.tellg()); if (raw != file_table.raw_sector_files.end()) { position = position >= raw->second.base ? position - raw->second.base : 0ull; } if (fd != 64 && std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { std::cerr << "[io] sceIoLseek fd=" << fd << " off=" << offset << " whence=" << whence << " -> " << position << (raw != file_table.raw_sector_files.end() ? " (raw)" : "") << "\n"; } ctx.set_gpr(2, static_cast(position)); ctx.set_gpr(3, static_cast(position >> 32u)); }); runtime.register_hle("IoFileMgrForUser", 0xACE946E8u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::string path = rt.memory().read_c_string(ctx.gpr[4]); const std::uint32_t stat_address = ctx.gpr[5]; if (stat_address == 0u || !rt.memory().contains(stat_address, 0x58u)) { ctx.set_gpr(2, 0x80010016u); return; } std::error_code error; const auto native = rt.translate_path(path); const bool directory = std::filesystem::is_directory(native, error); const bool regular = std::filesystem::is_regular_file(native, error); if (!directory && !regular) { ctx.set_gpr(2, 0x80010002u); return; } if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { std::cerr << "[io] sceIoGetstat \"" << path << "\"\n"; } rt.memory().zero(stat_address, 0x58u); rt.memory().store32(stat_address + 0x00u, (directory ? 0x1000u : 0x2000u) | 0x01FFu); rt.memory().store32(stat_address + 0x04u, directory ? 0x0010u : 0x0020u); const std::uint64_t size = regular ? static_cast(std::filesystem::file_size(native, error)) : 0u; rt.memory().store32(stat_address + 0x08u, static_cast(size)); rt.memory().store32(stat_address + 0x0Cu, static_cast(size >> 32u)); const auto written = std::filesystem::last_write_time(native, error); const auto system_time = std::chrono::clock_cast(written); const std::time_t seconds = std::chrono::system_clock::to_time_t(system_time); std::tm parts{}; #if defined(_WIN32) localtime_s(&parts, &seconds); #else localtime_r(&seconds, &parts); #endif for (std::uint32_t stamp : {0x10u, 0x20u, 0x30u}) { const std::uint32_t base = stat_address + stamp; rt.memory().store16(base + 0u, static_cast(parts.tm_year + 1900)); rt.memory().store16(base + 2u, static_cast(parts.tm_mon + 1)); rt.memory().store16(base + 4u, static_cast(parts.tm_mday)); rt.memory().store16(base + 6u, static_cast(parts.tm_hour)); rt.memory().store16(base + 8u, static_cast(parts.tm_min)); rt.memory().store16(base + 10u, static_cast(parts.tm_sec)); } set_success(ctx); }); runtime.register_hle("IoFileMgrForUser", 0xB29DDF9Cu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { try { const auto native = rt.translate_path(rt.memory().read_c_string(ctx.gpr[4])); if (!std::filesystem::is_directory(native)) { ctx.set_gpr(2, 0x80010002u); return; } DirectoryHandle handle; for (const auto &entry : std::filesystem::directory_iterator(native)) handle.entries.push_back(entry); std::sort(handle.entries.begin(), handle.entries.end(), [](const auto &a, const auto &b) { return a.path().filename().string() < b.path().filename().string(); }); const auto fd = file_table.next_fd++; file_table.directories.emplace(fd, std::move(handle)); ctx.set_gpr(2, static_cast(fd)); } catch (...) { ctx.set_gpr(2, 0x80010002u); } }); runtime.register_hle("IoFileMgrForUser", 0xE3EB004Cu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto fd = static_cast(ctx.gpr[4]); const std::uint32_t dirent = ctx.gpr[5]; const auto it = file_table.directories.find(fd); if (it == file_table.directories.end() || !rt.memory().contains(dirent, 0x160u)) { ctx.set_gpr(2, 0x80010009u); return; } if (it->second.index >= it->second.entries.size()) { ctx.set_gpr(2, 0u); return; } const auto &entry = it->second.entries[it->second.index++]; rt.memory().zero(dirent, 0x160u); const bool is_directory = entry.is_directory(); const std::uint32_t mode = is_directory ? 0x1000u : 0x2000u; rt.memory().store32(dirent, mode); if (!is_directory) { if (const auto *disc_file = register_virtual_disc_file(entry.path())) { rt.memory().store32(dirent + 8u, static_cast(disc_file->size)); rt.memory().store32(dirent + 12u, static_cast(disc_file->size >> 32u)); rt.memory().store32(dirent + 0x40u, disc_file->start_sector); if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { std::cerr << "[io] sceIoDread file=\"" << entry.path().filename().string() << "\" sector=" << disc_file->start_sector << " size=" << disc_file->size << "\n"; } } } const std::string name = entry.path().filename().string(); std::vector bytes(name.begin(), name.end()); bytes.push_back(0u); if (bytes.size() > 256u) bytes.resize(256u); rt.memory().copy_in(dirent + 0x58u, bytes); ctx.set_gpr(2, 1u); }); runtime.register_hle("IoFileMgrForUser", 0xEB092469u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto fd = static_cast(ctx.gpr[4]); ctx.set_gpr(2, file_table.directories.erase(fd) == 1u ? 0u : 0x80010009u); }); runtime.register_hle("LoadExecForUser", 0x4AC57943u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("sceCtrl", 0x6A2774F3u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t previous = controller_state.sampling_cycle; controller_state.sampling_cycle = ctx.gpr[4]; ctx.set_gpr(2, previous); }); runtime.register_hle("sceCtrl", 0x1F4011E6u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { if (ctx.gpr[4] > 1u) { ctx.set_gpr(2, 0x80000107u); return; } const std::uint32_t previous = controller_state.sampling_mode; controller_state.sampling_mode = ctx.gpr[4]; ctx.set_gpr(2, previous); }); runtime.register_hle("sceCtrl", 0x1F803938u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t destination = ctx.gpr[4]; const std::uint32_t count = ctx.gpr[5]; constexpr std::uint32_t sample_size = 16u; if (count == 0u) { ctx.set_gpr(2, 0u); return; } if (count > 64u || !rt.memory().contains(destination, static_cast(count) * sample_size)) { ctx.set_gpr(2, 0x80000103u); return; } const HostInputState host = display_window_input(); const std::uint32_t buttons = host.buttons; for (std::uint32_t index = 0u; index < count; ++index) { const std::uint32_t sample = destination + index * sample_size; rt.memory().store32(sample, static_cast(system_time_microseconds())); rt.memory().store32(sample + 4u, buttons); rt.memory().store8(sample + 8u, host.analog_x); rt.memory().store8(sample + 9u, host.analog_y); rt.memory().store8(sample + 10u, 128u); rt.memory().store8(sample + 11u, 128u); rt.memory().zero(sample + 12u, 4u); } ctx.set_gpr(2, count); }); runtime.register_hle("sceDisplay", 0x0E20F177u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t mode = ctx.gpr[4]; const std::uint32_t width = ctx.gpr[5]; const std::uint32_t height = ctx.gpr[6]; if (mode != 0u || width == 0u || width > 480u || height == 0u || height > 272u) { ctx.set_gpr(2, 0x80000107u); return; } display_state.mode = mode; display_state.width = width; display_state.height = height; set_success(ctx); }); runtime.register_hle("sceDisplay", 0x289D82FEu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t address = ctx.gpr[4]; const std::uint32_t stride = ctx.gpr[5]; const std::uint32_t format = ctx.gpr[6]; const std::uint32_t sync = ctx.gpr[7]; if (address != 0u && !rt.memory().contains(address, 4u)) { ctx.set_gpr(2, 0x800200D3u); return; } if (format > 3u || sync > 1u) { ctx.set_gpr(2, 0x80000107u); return; } if (std::getenv("LCS_TICK_DIAG") != nullptr && display_state.frame_buffer != address) { std::cerr << "[tick] SetFrameBuf " << psprecomp::hex32(address) << " stride=" << stride << " fmt=" << format << "\n"; } display_state.frame_buffer = address; display_state.buffer_width = stride; display_state.pixel_format = format; display_state.sync_mode = sync; set_success(ctx); }); runtime.register_hle("sceDisplay", 0x4D4E10ECu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint64_t blank = kVblankPeriodUs / 5u; const std::uint64_t phase = virtual_time_us % kVblankPeriodUs; ctx.set_gpr(2, phase < blank ? 1u : 0u); }); runtime.register_hle("sceDisplay", 0x9C6EAAD7u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { if (std::getenv("LCS_TICK_DIAG") != nullptr) { std::cerr << "[tick] GetVcount uid=" << thread_table.current_uid << " a0=" << ctx.gpr[4] << " s5=" << ctx.gpr[21] << " vblank=" << display_vblank_index << "\n"; } ctx.set_gpr(2, static_cast(display_vblank_index)); }); auto wait_vblank = [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { ++display_vblank_index; check_wall_clock_limit(rt, 0x3Fu); reset_pc_profile_on_key(); dump_ram_if_requested(rt.memory()); if (std::getenv("LCS_TICK_DIAG") != nullptr) { std::cerr << "[tick] WaitVblank uid=" << thread_table.current_uid << " ra=" << psprecomp::hex32(ctx.gpr[31]) << " vblank=" << display_vblank_index << " fb=" << psprecomp::hex32(display_state.frame_buffer); if (thread_table.current_uid == 3 && rt.memory().contains(0x08B56920u, 4u) && rt.memory().contains(0x08B56950u, 4u)) { std::cerr << " gate0x6920=" << psprecomp::hex32(rt.memory().load32(0x08B56920u)) << " gate0x6950=" << psprecomp::hex32(rt.memory().load32(0x08B56950u)) << " skip691c=" << static_cast(rt.memory().load8(0x08B5691Cu)) << " skip691d=" << static_cast(rt.memory().load8(0x08B5691Du)); } std::cerr << "\n"; } PresentRequest request{}; request.vblank = display_vblank_index; request.display_buffer = display_state.frame_buffer; request.display_stride = display_state.buffer_width; request.pixel_format = display_state.pixel_format; request.display_width = display_state.width; request.display_height = display_state.height; if (rt.memory().contains(kGameRenderWidthAddress, 8u)) { request.game_width = rt.memory().load32(kGameRenderWidthAddress); request.game_height = rt.memory().load32(kGameRenderWidthAddress + 4u); } if (!ge_async_enabled()) { present_frame(rt, request); } else if (ge_worker.pending_presents.load() == 0u) { ge_worker.pending_presents.fetch_add(1u); ge_worker_submit([&rt, request] { present_frame(rt, request); ge_worker.pending_presents.fetch_sub(1u); }); } display_window_pump(); if (display_window_closed()) { ge_worker_wait_idle(); rt.stop("Display window closed"); return; } audio_output_advance(virtual_time_us); const auto throttle_started = std::chrono::steady_clock::now(); if (const std::uint32_t skipped = throttle_vblank_to_real_time(); skipped != 0u) { virtual_time_us += static_cast(skipped) * kVblankPeriodUs; display_vblank_index += skipped; } g_speed_throttle_ns += static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now() - throttle_started).count()); report_realtime_speed_if_requested(rt, display_vblank_index); static const bool fixed_delay = std::getenv("LCS_VBLANK_FIXED_DELAY") != nullptr; const std::uint32_t vblank_delay = fixed_delay ? static_cast(kVblankPeriodUs) : static_cast((virtual_time_us / kVblankPeriodUs + 1u) * kVblankPeriodUs - virtual_time_us); if (deliver_pending_ge_callback(ctx, vblank_delay, true)) return; if (dispatch_vblank_interrupt(rt, ctx, vblank_delay)) return; (void)delay_current_thread(rt, ctx, vblank_delay); }; runtime.register_hle("sceDisplay", 0x36CDFADEu, wait_vblank); runtime.register_hle("sceDisplay", 0x984C27E7u, wait_vblank); runtime.register_hle("sceGe_user", 0x1F6752ADu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, 0x00200000u); }); runtime.register_hle("sceGe_user", 0xE47E40E4u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, 0x04000000u); }); runtime.register_hle("sceGe_user", 0xAB49E76Au, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { if (std::getenv("LCS_GE_COUNT_DIAG") != nullptr) std::cerr << "[ge] list #" << (ge_next_list_id) << " enqueued\n"; if (std::getenv("LCS_TICK_DIAG") != nullptr) std::cerr << "[tick] sceGeListEnQueue list=0x" << psprecomp::hex32(ctx.gpr[4]) << "\n"; const std::uint32_t list_address = ctx.gpr[4]; static const bool draw_vblank_lists = std::getenv("LCS_DRAW_VBLANK_LISTS") != nullptr; const bool from_vblank = !draw_vblank_lists && in_vblank_interrupt(); if (!from_vblank) cap_frame_rate(list_address); bool list_finished = false; std::uint32_t finish_argument = 0u; if (from_vblank) { const GeListPrescan scan = prescan_ge_list(rt.memory(), list_address); list_finished = scan.finished; finish_argument = scan.finish_argument; } else if (ge_worker_wait_idle(); ge_async_enabled()) { const GeListPrescan scan = prescan_ge_list(rt.memory(), list_address); list_finished = scan.finished; finish_argument = scan.finish_argument; const std::uint64_t vblank = display_vblank_index; ge_worker_submit([&rt, list_address, vblank] { const auto list_started = std::chrono::steady_clock::now(); execute_ge_list_frame(rt, list_address, vblank); g_speed_ge_list_ns += static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now() - list_started).count()); }); } else { const auto list_started = std::chrono::steady_clock::now(); execute_ge_list_frame(rt, list_address, display_vblank_index); g_speed_ge_list_ns += static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now() - list_started).count()); list_finished = rendered_list_finished(); finish_argument = rendered_finish_argument(); } const auto list_id = static_cast(ge_next_list_id++); const auto cbid = static_cast(ctx.gpr[6]); const auto found = ge_callback_table.callbacks.find(cbid); hang_trace("enqueue list=" + psprecomp::hex32(list_address) + " from_vblank=" + std::to_string(from_vblank ? 1 : 0) + " finished=" + std::to_string(list_finished ? 1 : 0) + " finish_arg=" + std::to_string(finish_argument) + " cbid=" + std::to_string(cbid) + " ra=" + psprecomp::hex32(ctx.gpr[31])); if (std::getenv("LCS_TICK_DIAG") != nullptr) std::cerr << "[tick] sceGeListEnQueue cbid=" << cbid << " found=" << (found != ge_callback_table.callbacks.end()) << " finish_fn=0x" << psprecomp::hex32(found != ge_callback_table.callbacks.end() ? found->second.finish_function : 0u) << "\n"; if (std::getenv("LCS_TICK_DIAG") != nullptr) std::cerr << "[tick] sceGeListEnQueue finished=" << list_finished << " finish_arg=" << finish_argument << "\n"; if (found != ge_callback_table.callbacks.end() && found->second.finish_function != 0u && list_finished) { pending_ge_callbacks[thread_table.current_uid].push_back(PendingGeCallback{ cbid, found->second.finish_function, finish_argument, found->second.finish_argument}); } ctx.set_gpr(2, list_id); }); runtime.register_hle("sceGe_user", 0xB287BD61u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ge_worker_wait_idle(); ctx.set_gpr(2, 0u); }); runtime.register_hle("sceGe_user", 0xA4FC06A4u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t callback_data = ctx.gpr[4]; if (callback_data == 0u || !rt.memory().contains(callback_data, 16u)) { ctx.set_gpr(2, 0x800200D3u); return; } GeCallbackRecord record{ rt.memory().load32(callback_data + 0u), rt.memory().load32(callback_data + 4u), rt.memory().load32(callback_data + 8u), rt.memory().load32(callback_data + 12u), }; const std::int32_t uid = ge_callback_table.next_uid++; ge_callback_table.callbacks.emplace(uid, record); if (std::getenv("LCS_TICK_DIAG") != nullptr) std::cerr << "[tick] sceGeSetCallback uid=" << uid << " finish_fn=0x" << psprecomp::hex32(record.finish_function) << "\n"; ctx.set_gpr(2, static_cast(uid)); }); runtime.register_hle("sceGe_user", 0x05DB22CEu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::int32_t uid = static_cast(ctx.gpr[4]); ctx.set_gpr(2, ge_callback_table.callbacks.erase(uid) == 1u ? 0u : 0x80000100u); }); auto volatile_mem_lock = [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { constexpr std::uint32_t volatile_base = 0x08400000u; constexpr std::uint32_t volatile_size = 0x00400000u; if (ctx.gpr[4] != 0u) { ctx.set_gpr(2, 0x80000107u); return; } if (volatile_memory_locked) { ctx.set_gpr(2, 0x80000021u); return; } if (!rt.memory().contains(volatile_base, volatile_size) || !rt.memory().contains(ctx.gpr[5], 4u) || !rt.memory().contains(ctx.gpr[6], 4u)) { ctx.set_gpr(2, 0x800200D3u); return; } rt.memory().store32(ctx.gpr[5], volatile_base); rt.memory().store32(ctx.gpr[6], volatile_size); rt.memory().zero(volatile_base, volatile_size); volatile_memory_locked = true; set_success(ctx); }; runtime.register_hle("sceSuspendForUser", 0x3E0271D3u, volatile_mem_lock); runtime.register_hle("sceSuspendForUser", 0xA14F40B2u, volatile_mem_lock); runtime.register_hle("sceSuspendForUser", 0xA569E425u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { if (ctx.gpr[4] != 0u) { ctx.set_gpr(2, 0x80000107u); return; } if (!volatile_memory_locked) { ctx.set_gpr(2, 0x800201AEu); return; } volatile_memory_locked = false; set_success(ctx); }); runtime.register_hle("sceSuspendForUser", 0xEADB1BD7u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, ctx.gpr[4] == 0u ? 0u : 0x80000107u); }); runtime.register_hle("sceSuspendForUser", 0x3AEE7261u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, ctx.gpr[4] == 0u ? 0u : 0x80000107u); }); runtime.register_hle("sceSuspendForUser", 0x090CCB3Fu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("UtilsForUser", 0x6AD345D7u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { general_purpose_io = ctx.gpr[4]; set_success(ctx); }); auto cache_maintenance = [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }; runtime.register_hle("UtilsForUser", 0x79D1C3FAu, cache_maintenance); runtime.register_hle("UtilsForUser", 0xB435DEC5u, cache_maintenance); runtime.register_hle("sceUtility", 0x50C4CD57u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t parameter = ctx.gpr[4]; if (savedata_utility.status != UtilityStatus::None) { ctx.set_gpr(2, 0x80110001u); return; } if (parameter == 0u || !rt.memory().contains(parameter, 4u)) { ctx.set_gpr(2, 0x80110004u); return; } const std::uint32_t declared_size = rt.memory().load32(parameter); if (declared_size < 0x5C0u || !rt.memory().contains(parameter, std::min(declared_size, kSavedataParameterMinimumSize))) { ctx.set_gpr(2, 0x80110004u); return; } savedata_utility = SavedataUtilityState{UtilityStatus::Init, parameter, false}; rt.memory().store32(parameter + kUtilityCommonResultOffset, 0u); set_success(ctx); }); runtime.register_hle("sceUtility", 0xD4B95FFBu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { if (savedata_utility.status == UtilityStatus::None || savedata_utility.status == UtilityStatus::Finished) { ctx.set_gpr(2, 0x80110001u); return; } if (savedata_utility.status == UtilityStatus::Init) { savedata_utility.status = UtilityStatus::Visible; } else if (savedata_utility.status == UtilityStatus::Visible && !savedata_utility.operation_complete) { const std::uint32_t result = execute_savedata_operation(rt, savedata_utility.parameter_address); rt.memory().store32(savedata_utility.parameter_address + kUtilityCommonResultOffset, result); savedata_utility.operation_complete = true; savedata_utility.status = UtilityStatus::Quit; } set_success(ctx); }); runtime.register_hle("sceUtility", 0x8874DBE0u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const UtilityStatus reported = savedata_utility.status; ctx.set_gpr(2, static_cast(reported)); if (reported == UtilityStatus::Init) { savedata_utility.status = UtilityStatus::Visible; } else if (reported == UtilityStatus::Finished) { savedata_utility = SavedataUtilityState{}; } }); runtime.register_hle("sceUtility", 0x9790B33Cu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { if (savedata_utility.status != UtilityStatus::Quit) { ctx.set_gpr(2, 0x80110001u); return; } savedata_utility.status = UtilityStatus::Finished; set_success(ctx); }); runtime.register_hle("sceUtility", 0xA5DA2406u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t id = ctx.gpr[4]; const std::uint32_t dest = ctx.gpr[5]; if (!rt.memory().contains(dest, 4u)) { ctx.set_gpr(2, 0x80110103u); return; } std::uint32_t value = 0u; switch (id) { case 2u: value = 0u; break; // ADHOC_CHANNEL: automatic case 3u: value = 0u; break; // WLAN_POWERSAVE: off case 4u: value = 0u; break; // DATE_FORMAT: YYYYMMDD case 5u: value = 0u; break; // TIME_FORMAT: 24HR case 6u: value = 0u; break; // TIMEZONE: UTC case 7u: value = 0u; break; // DAYLIGHTSAVINGS: std case 8u: value = 1u; break; // LANGUAGE: English case 9u: value = 0u; break; // BUTTON_SWAP: circle-confirm default: ctx.set_gpr(2, 0x80110103u); return; } rt.memory().store32(dest, value); set_success(ctx); }); runtime.register_hle("sceUtility", 0x34B78343u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t id = ctx.gpr[4]; const std::uint32_t dest = ctx.gpr[5]; if (id != 1u || !rt.memory().contains(dest, 128u)) { ctx.set_gpr(2, 0x80110103u); return; } rt.memory().zero(dest, 128u); const std::string nickname = "PLAYER"; rt.memory().copy_in(dest, std::vector(nickname.begin(), nickname.end())); set_success(ctx); }); runtime.register_hle("sceUtility", 0x1579A159u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, static_cast(next_module_uid++)); }); runtime.register_hle("sceUtility", 0x64D50C56u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("InterruptManager", 0xCA04A2B9u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t interrupt_number = ctx.gpr[4]; const std::uint32_t sub_number = ctx.gpr[5]; const std::uint32_t handler = ctx.gpr[6]; const std::uint32_t argument = ctx.gpr[7]; if (interrupt_number >= 67u || handler == 0u) { ctx.set_gpr(2, 0x80020064u); return; } const std::uint64_t key = sub_interrupt_key(interrupt_number, sub_number); if (sub_interrupts.contains(key)) { ctx.set_gpr(2, 0x80020067u); return; } sub_interrupts.emplace(key, SubInterruptRecord{handler, argument, false}); std::cerr << "[lcs] RegisterSubIntrHandler intr=" << interrupt_number << " sub=" << sub_number << " handler=0x" << std::hex << handler << " arg=0x" << argument << std::dec << "\n"; set_success(ctx); }); runtime.register_hle("InterruptManager", 0xD61E6961u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint64_t key = sub_interrupt_key(ctx.gpr[4], ctx.gpr[5]); ctx.set_gpr(2, sub_interrupts.erase(key) == 1u ? 0u : 0x80020068u); }); runtime.register_hle("InterruptManager", 0xFB8E22ECu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto found = sub_interrupts.find(sub_interrupt_key(ctx.gpr[4], ctx.gpr[5])); if (found == sub_interrupts.end()) { ctx.set_gpr(2, 0x80020068u); return; } found->second.enabled = true; set_success(ctx); }); runtime.register_hle("sceMpeg", 0x682A619Bu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("sceMpeg", 0x874624D6u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("sceMpeg", 0xD7A29F46u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto packets = static_cast(ctx.gpr[4]); if (packets < 0) { ctx.set_gpr(2, 0x80610103u); return; } ctx.set_gpr(2, static_cast(packets) * (2048u + 104u)); }); runtime.register_hle("sceMpeg", 0xC132E22Fu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, 0x00010000u); }); runtime.register_hle("sceMpeg", 0x37295ED8u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t ring = ctx.gpr[4]; const auto packets = static_cast(ctx.gpr[5]); const std::uint32_t data = ctx.gpr[6]; const std::uint32_t size = ctx.gpr[7]; const std::uint32_t callback = ctx.gpr[8]; const std::uint32_t callback_arg = ctx.gpr[9]; if (packets < 0 || !rt.memory().contains(ring, 48u)) { ctx.set_gpr(2, 0x800200D3u); return; } const std::uint64_t required = static_cast(packets) * (2048u + 104u); if (required > size || !rt.memory().contains(data, static_cast(packets) * 2048u)) { ctx.set_gpr(2, 0x80610103u); return; } rt.memory().zero(ring, 48u); rt.memory().store32(ring + 0u, static_cast(packets)); rt.memory().store32(ring + 16u, 2048u); rt.memory().store32(ring + 20u, data); rt.memory().store32(ring + 24u, callback); rt.memory().store32(ring + 28u, callback_arg); rt.memory().store32(ring + 32u, data + static_cast(packets) * 2048u); rt.memory().store32(ring + 44u, ctx.gpr[28]); set_success(ctx); }); runtime.register_hle("sceMpeg", 0xD8C5F121u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { if (std::getenv("LCS_SKIP_MPEG") != nullptr) { ctx.set_gpr(2, 0x80610003u); return; } const std::uint32_t mpeg_out = ctx.gpr[4]; const std::uint32_t data = ctx.gpr[5]; const std::uint32_t size = ctx.gpr[6]; const std::uint32_t ring = ctx.gpr[7]; if (size < 0x10000u || !rt.memory().contains(mpeg_out, 4u) || !rt.memory().contains(data, size) || !rt.memory().contains(ring, 48u)) { ctx.set_gpr(2, 0x80610103u); return; } const std::uint32_t handle = data + 0x30u; if (!rt.memory().contains(handle, 24u)) { ctx.set_gpr(2, 0x800200D3u); return; } rt.memory().store32(mpeg_out, handle); const std::array magic{'L', 'I', 'B', 'M', 'P', 'E', 'G', 0}; const std::array version{'0', '0', '1', 0}; rt.memory().copy_in(handle, magic); rt.memory().copy_in(handle + 8u, version); rt.memory().store32(handle + 12u, 0xFFFFFFFFu); rt.memory().store32(handle + 16u, ring); rt.memory().store32(handle + 20u, rt.memory().load32(ring + 32u)); rt.memory().store32(ring + 40u, mpeg_out); MpegContextState created{}; created.ring_address = ring; mpeg_contexts.erase(mpeg_out); mpeg_contexts.emplace(mpeg_out, std::move(created)); set_success(ctx); }); runtime.register_hle("sceMpeg", 0x21FF80E4u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t mpeg = ctx.gpr[4]; const std::uint32_t buffer = ctx.gpr[5]; const std::uint32_t output = ctx.gpr[6]; if (!mpeg_contexts.contains(mpeg) || !rt.memory().contains(buffer, 2048u) || !rt.memory().contains(output, 4u)) { ctx.set_gpr(2, 0x80610103u); return; } std::array bytes{}; rt.memory().copy_out(buffer, bytes); ParsedPsmfHeader header{}; if (!parse_psmf_header(bytes, header) || header.stream_offset == 0u || (header.stream_offset & 2047u) != 0u) { rt.memory().store32(output, 0u); ctx.set_gpr(2, 0x806101FEu); return; } if (const auto state = mpeg_contexts.find(mpeg); state != mpeg_contexts.end()) { const ParsedPsmfHeader &previous = state->second.header; const bool different_stream = state->second.analyzed && (previous.stream_offset != header.stream_offset || previous.stream_size != header.stream_size || previous.first_timestamp != header.first_timestamp || previous.last_timestamp != header.last_timestamp); if (different_stream) { state->second.video.close(); state->second.audio.close(); state->second.source_path.clear(); state->second.audio_source.clear(); state->second.video_eof = false; state->second.video_au_count = 0u; state->second.audio_au_count = 0u; } state->second.header = header; state->second.analyzed = true; if (state->second.source_path.empty()) { state->second.source_path = identify_pmf_source(header); if (state->second.source_path.empty()) { state->second.source_path = find_pmf_on_disc(rt.translate_path("disc0:/"), header); } } if (std::getenv("LCS_MPEG_DIAG") != nullptr) { std::cerr << "[mpeg] header " << header.width << "x" << header.height << " offset=" << header.stream_offset << " size=" << header.stream_size << " source=\"" << state->second.source_path.string() << "\"\n"; } } rt.memory().store32(output, header.stream_offset); set_success(ctx); }); runtime.register_hle("sceMpeg", 0x611E9E11u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t buffer = ctx.gpr[4]; const std::uint32_t output = ctx.gpr[5]; if (!rt.memory().contains(buffer, 2048u) || !rt.memory().contains(output, 4u)) { ctx.set_gpr(2, 0x80610103u); return; } std::array bytes{}; rt.memory().copy_out(buffer, bytes); ParsedPsmfHeader header{}; if (!parse_psmf_header(bytes, header) || (header.stream_offset & 2047u) != 0u) { rt.memory().store32(output, 0u); ctx.set_gpr(2, 0x806101FEu); return; } rt.memory().store32(output, header.stream_size); set_success(ctx); }); runtime.register_hle("sceMpeg", 0x13407F13u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t ring = ctx.gpr[4]; if (ring != 0u && rt.memory().contains(ring, 48u)) { rt.memory().store32(ring + 12u, 0u); rt.memory().store32(ring + 40u, 0u); } set_success(ctx); }); runtime.register_hle("sceMpeg", 0x606A4649u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t mpeg_out = ctx.gpr[4]; if (mpeg_out == 0u || !rt.memory().contains(mpeg_out, 4u)) { ctx.set_gpr(2, 0x800200D3u); return; } mpeg_contexts.erase(mpeg_out); set_success(ctx); }); constexpr std::uint32_t kAtracErrorApiFail = 0x80630002u; constexpr std::uint32_t kAtracErrorNoId = 0x80630003u; constexpr std::uint32_t kAtracErrorBadId = 0x80630005u; constexpr std::uint32_t kAtracErrorUnknownFormat = 0x80630006u; constexpr std::uint32_t kAtracErrorAllDataLoaded = 0x80630009u; constexpr std::uint32_t kAtracErrorIncorrectReadSize = 0x80630013u; constexpr std::uint32_t kAtracErrorBadAddress = 0x800200D3u; const auto get_atrac = [](std::uint32_t id) -> AtracContextState * { if (id >= atrac_contexts.size() || !atrac_contexts[id].allocated) { if (atrac_diag_enabled()) std::cerr << "[atrac] bad id " << static_cast(id) << " uid=" << thread_table.current_uid << " t=" << virtual_time_us << "\n"; return nullptr; } return &atrac_contexts[id]; }; const auto atrac_fail = [](psprecomp::AllegrexContext &ctx, std::uint32_t code, const char *where) { if (atrac_diag_enabled()) std::cerr << "[atrac] " << where << " failed " << psprecomp::hex32(code) << " uid=" << thread_table.current_uid << " t=" << virtual_time_us << "\n"; ctx.set_gpr(2, code); }; runtime.register_hle("sceAtrac3plus", 0x0FAE370Eu, [atrac_fail](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t buffer = ctx.gpr[4]; const std::uint32_t read_size = ctx.gpr[5]; const std::uint32_t buffer_size = ctx.gpr[6]; if (read_size > buffer_size) { atrac_fail(ctx, kAtracErrorIncorrectReadSize, "set-halfway size"); return; } if (read_size < 12u || !rt.memory().contains(buffer, read_size)) { atrac_fail(ctx, kAtracErrorUnknownFormat, "set-halfway buffer"); return; } std::vector header_bytes(read_size); rt.memory().copy_out(buffer, header_bytes); ParsedAtracHeader parsed{}; if (!parse_atrac_header(header_bytes, parsed)) { atrac_fail(ctx, kAtracErrorUnknownFormat, "set-halfway header"); return; } std::size_t id = atrac_contexts.size(); for (std::size_t i = 0u; i < atrac_contexts.size(); ++i) { if (!atrac_contexts[i].allocated) { id = i; break; } } if (id == atrac_contexts.size()) { atrac_fail(ctx, kAtracErrorNoId, "set-halfway no free id"); return; } auto &state = atrac_contexts[id]; close_atrac_decoder(state); state = AtracContextState{}; state.allocated = true; state.header = parsed; state.buffer_address = buffer; state.initial_read_size = read_size; state.buffer_size = buffer_size; state.buffered_encoded_bytes = read_size > parsed.data_offset ? read_size - parsed.data_offset : 0u; state.buffered_encoded_bytes = std::min(state.buffered_encoded_bytes, parsed.data_size); state.next_file_offset = std::min(read_size, parsed.file_size); state.write_offset = buffer_size == 0u ? 0u : read_size % buffer_size; state.source_path = identify_atrac_source(rt.translate_path("disc0:/"), header_bytes, parsed); if (atrac_diag_enabled() || state.source_path.empty()) { std::cerr << "[atrac] set-halfway id=" << id << " buffer=" << psprecomp::hex32(buffer) << " read=" << read_size << " capacity=" << buffer_size << " file=" << parsed.file_size << " frame=" << parsed.block_align << " rate=" << parsed.sample_rate << " channels=" << parsed.channels << " samples=" << parsed.total_samples << " source=\"" << state.source_path.string() << "\"\n"; } ctx.set_gpr(2, static_cast(id)); }); runtime.register_hle("sceAtrac3plus", 0x61EB33F5u, [get_atrac](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } close_atrac_decoder(*state); *state = AtracContextState{}; set_success(ctx); }); runtime.register_hle("sceAtrac3plus", 0x5D268707u, [get_atrac](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } const std::uint32_t write_ptr_addr = ctx.gpr[5]; const std::uint32_t writable_addr = ctx.gpr[6]; const std::uint32_t read_offset_addr = ctx.gpr[7]; for (const std::uint32_t address : {write_ptr_addr, writable_addr, read_offset_addr}) { if (address != 0u && !rt.memory().contains(address, 4u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } } const std::uint32_t remaining_file = state->next_file_offset < state->header.file_size ? state->header.file_size - state->next_file_offset : 0u; const std::uint32_t free_bytes = state->buffer_size > state->buffered_encoded_bytes ? state->buffer_size - state->buffered_encoded_bytes : 0u; const std::uint32_t contiguous = state->buffer_size == 0u ? 0u : state->buffer_size - state->write_offset; const std::uint32_t writable = std::min({remaining_file, free_bytes, contiguous}); state->last_writable_bytes = writable; if (write_ptr_addr != 0u) rt.memory().store32(write_ptr_addr, state->buffer_address + state->write_offset); if (writable_addr != 0u) rt.memory().store32(writable_addr, writable); if (read_offset_addr != 0u) rt.memory().store32(read_offset_addr, state->next_file_offset); set_success(ctx); }); runtime.register_hle("sceAtrac3plus", 0x7DB31251u, [get_atrac](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } const std::uint32_t bytes = ctx.gpr[5]; if (state->next_file_offset >= state->header.file_size) { ctx.set_gpr(2, bytes == 0u ? 0u : kAtracErrorAllDataLoaded); return; } if (bytes > state->last_writable_bytes) { ctx.set_gpr(2, kAtracErrorIncorrectReadSize); return; } state->buffered_encoded_bytes = std::min(state->buffer_size, state->buffered_encoded_bytes + bytes); state->next_file_offset = std::min(state->header.file_size, state->next_file_offset + bytes); if (state->buffer_size != 0u) state->write_offset = (state->write_offset + bytes) % state->buffer_size; state->last_writable_bytes = 0u; set_success(ctx); }); runtime.register_hle("sceAtrac3plus", 0x6A8C3CD5u, [get_atrac](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } const std::uint32_t output = ctx.gpr[5]; const std::uint32_t samples_addr = ctx.gpr[6]; const std::uint32_t finish_addr = ctx.gpr[7]; const std::uint32_t remain_addr = ctx.gpr[8]; for (const std::uint32_t address : {samples_addr, finish_addr, remain_addr}) { if (address != 0u && !rt.memory().contains(address, 4u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } } const std::uint32_t max_samples = atrac_samples_per_frame(*state); const std::size_t max_bytes = static_cast(max_samples) * kAtracOutputChannels * 2u; if (output != 0u && !rt.memory().contains(output, max_bytes)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } if (state->source_path.empty()) { state->internal_error = kAtracErrorUnknownFormat; if (atrac_diag_enabled()) std::cerr << "[atrac] decode id=" << ctx.gpr[4] << " failed: unidentified source\n"; ctx.set_gpr(2, kAtracErrorApiFail); return; } auto restart_for_loop = [&]() -> bool { if (state->loop_num == 0) return false; if (state->header.loop_start < 0) return false; if (state->loop_num > 0) --state->loop_num; state->sample_position = static_cast(state->header.loop_start); close_atrac_decoder(*state); return open_atrac_decoder(*state); }; if (state->sample_position >= state->header.total_samples && !restart_for_loop()) { if (samples_addr != 0u) rt.memory().store32(samples_addr, 0u); if (finish_addr != 0u) rt.memory().store32(finish_addr, 1u); if (remain_addr != 0u) rt.memory().store32(remain_addr, 0u); set_success(ctx); return; } const std::uint32_t requested_samples = static_cast(std::min( max_samples, state->header.total_samples - state->sample_position)); static std::vector pcm; const std::size_t pcm_bytes = static_cast(requested_samples) * kAtracOutputChannels * 2u; if (pcm.size() < pcm_bytes) pcm.resize(pcm_bytes); const std::span pcm_span(pcm.data(), pcm_bytes); std::size_t got = read_atrac_pcm(*state, pcm_span); if (got == 0u && restart_for_loop()) got = read_atrac_pcm(*state, pcm_span); const std::size_t bytes_per_sample = static_cast(kAtracOutputChannels) * 2u; const std::uint32_t samples = static_cast(got / bytes_per_sample); got = static_cast(samples) * bytes_per_sample; if (output != 0u && got != 0u) rt.memory().copy_in(output, std::span(pcm.data(), got)); state->sample_position += samples; if (state->buffered_encoded_bytes >= state->header.block_align) state->buffered_encoded_bytes -= state->header.block_align; else state->buffered_encoded_bytes = 0u; const bool finished = samples == 0u || (state->sample_position >= state->header.total_samples && state->loop_num == 0); const std::uint32_t remaining_frames = state->header.block_align == 0u ? 0u : state->buffered_encoded_bytes / state->header.block_align; if (samples_addr != 0u) rt.memory().store32(samples_addr, samples); if (finish_addr != 0u) rt.memory().store32(finish_addr, finished ? 1u : 0u); if (remain_addr != 0u) rt.memory().store32(remain_addr, remaining_frames); if (atrac_diag_enabled()) { std::cerr << "[atrac] decode id=" << ctx.gpr[4] << " samples=" << samples << " position=" << state->sample_position << " finish=" << finished << " buffered_frames=" << remaining_frames << "\n"; } set_success(ctx); }); runtime.register_hle("sceAtrac3plus", 0x9AE849A7u, [get_atrac](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } if (!rt.memory().contains(ctx.gpr[5], 4u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } const std::uint32_t remaining = state->next_file_offset >= state->header.file_size ? 0xFFFFFFFFu : state->buffered_encoded_bytes / state->header.block_align; rt.memory().store32(ctx.gpr[5], remaining); set_success(ctx); }); runtime.register_hle("sceAtrac3plus", 0xA554A158u, [get_atrac](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } if (!rt.memory().contains(ctx.gpr[5], 4u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } rt.memory().store32(ctx.gpr[5], atrac_bitrate_kbps(*state)); set_success(ctx); }); runtime.register_hle("sceAtrac3plus", 0xA2BBA8BEu, [get_atrac](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } const std::array, 3> outputs{{ {ctx.gpr[5], state->header.total_samples == 0u ? 0u : state->header.total_samples - 1u}, {ctx.gpr[6], static_cast(state->header.loop_start)}, {ctx.gpr[7], static_cast(state->header.loop_end)}, }}; for (const auto &[address, value] : outputs) { if (address != 0u) { if (!rt.memory().contains(address, 4u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } rt.memory().store32(address, value); } } set_success(ctx); }); runtime.register_hle("sceAtrac3plus", 0xFAA4F89Bu, [get_atrac](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } if (ctx.gpr[5] != 0u) { if (!rt.memory().contains(ctx.gpr[5], 4u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } rt.memory().store32(ctx.gpr[5], static_cast(state->loop_num)); } if (ctx.gpr[6] != 0u) { if (!rt.memory().contains(ctx.gpr[6], 4u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } rt.memory().store32(ctx.gpr[6], state->header.loop_start >= 0 ? 1u : 0u); } set_success(ctx); }); runtime.register_hle("sceAtrac3plus", 0x868120B5u, [get_atrac](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } state->loop_num = static_cast(ctx.gpr[5]); set_success(ctx); }); runtime.register_hle("sceAtrac3plus", 0xE88F759Bu, [get_atrac](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } if (ctx.gpr[5] != 0u) { if (!rt.memory().contains(ctx.gpr[5], 4u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } rt.memory().store32(ctx.gpr[5], state->internal_error); } set_success(ctx); }); const auto buffer_info_for_resetting = [get_atrac](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } const std::uint32_t sample = ctx.gpr[5]; const std::uint32_t info = ctx.gpr[6]; if (!rt.memory().contains(info, 32u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; } const std::uint32_t frame = sample / atrac_samples_per_frame(*state); const std::uint64_t pos64 = static_cast(state->header.data_offset) + static_cast(frame) * state->header.block_align; const std::uint32_t file_pos = static_cast(std::min(pos64, state->header.file_size)); const std::uint32_t writable = std::min(state->buffer_size, state->header.file_size - file_pos); rt.memory().store32(info + 0u, state->buffer_address); rt.memory().store32(info + 4u, writable); rt.memory().store32(info + 8u, std::min(writable, state->header.block_align)); rt.memory().store32(info + 12u, file_pos); rt.memory().zero(info + 16u, 16u); set_success(ctx); }; runtime.register_hle("sceAtrac3plus", 0xCA3CA3D2u, buffer_info_for_resetting); runtime.register_hle("sceAtrac3plus", 0x2DD3E298u, buffer_info_for_resetting); runtime.register_hle("sceAtrac3plus", 0x644E5607u, [get_atrac](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { auto *state = get_atrac(ctx.gpr[4]); if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; } const std::uint32_t sample = std::min(ctx.gpr[5], state->header.total_samples); const std::uint32_t bytes_first = ctx.gpr[6]; const std::uint32_t frame = sample / atrac_samples_per_frame(*state); const std::uint64_t pos64 = static_cast(state->header.data_offset) + static_cast(frame) * state->header.block_align; state->sample_position = sample; state->next_file_offset = static_cast(std::min(pos64 + bytes_first, state->header.file_size)); state->buffered_encoded_bytes = std::min(bytes_first, state->buffer_size); state->write_offset = state->buffer_size == 0u ? 0u : bytes_first % state->buffer_size; close_atrac_decoder(*state); set_success(ctx); }); register_sas_hle(runtime); runtime.register_hle("sceAudio", 0x5EC81C55u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { auto channel = static_cast(ctx.gpr[4]); const std::uint32_t sample_count = ctx.gpr[5]; const std::uint32_t format = ctx.gpr[6]; if (sample_count == 0u || (sample_count & 63u) != 0u || sample_count > 65472u) { ctx.set_gpr(2, 0x80260006u); return; } if (channel < 0) { channel = -1; for (std::int32_t candidate = 0; candidate < 8; ++candidate) { if (!audio_channels[static_cast(candidate)].reserved) { channel = candidate; break; } } if (channel < 0) { ctx.set_gpr(2, 0x80260001u); return; } } else if (channel >= 8 || audio_channels[static_cast(channel)].reserved) { ctx.set_gpr(2, 0x80260001u); return; } auto &state = audio_channels[static_cast(channel)]; audio_output_reset_channel(static_cast(channel)); state = AudioChannelState{}; state.reserved = true; state.sample_count = sample_count; state.format = format; ctx.set_gpr(2, static_cast(channel)); }); runtime.register_hle("sceAudio", 0x6FC46853u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t channel = ctx.gpr[4]; if (channel >= 8u) { ctx.set_gpr(2, 0x80260003u); return; } if (!audio_channels[channel].reserved) { ctx.set_gpr(2, 0x80260001u); return; } audio_channels[channel] = {}; audio_output_reset_channel(channel); set_success(ctx); }); runtime.register_hle("sceAudio", 0xB011922Fu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t channel = ctx.gpr[4]; if (channel >= 8u) { ctx.set_gpr(2, 0x80260003u); return; } ctx.set_gpr(2, audio_remaining_samples(audio_channels[channel])); }); runtime.register_hle("sceAudio", 0xCB2E439Eu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t channel = ctx.gpr[4]; const std::uint32_t length = ctx.gpr[5]; if (channel >= 8u) { ctx.set_gpr(2, 0x80260003u); return; } if (!audio_channels[channel].reserved) { ctx.set_gpr(2, 0x80260001u); return; } if (length == 0u || (length & 63u) != 0u || length > 65472u) { ctx.set_gpr(2, 0x80260006u); return; } audio_channels[channel].sample_count = length; set_success(ctx); }); runtime.register_hle("sceAudio", 0x95FD0C2Du, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t channel = ctx.gpr[4]; if (channel >= 8u) { ctx.set_gpr(2, 0x80260003u); return; } if (!audio_channels[channel].reserved) { ctx.set_gpr(2, 0x80260001u); return; } audio_channels[channel].format = ctx.gpr[5]; set_success(ctx); }); runtime.register_hle("sceAudio", 0xB7E1D8E7u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const std::uint32_t channel = ctx.gpr[4]; if (channel >= 8u) { ctx.set_gpr(2, 0x80260003u); return; } if (!audio_channels[channel].reserved) { ctx.set_gpr(2, 0x80260001u); return; } set_success(ctx); }); auto audio_output_common = [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx, std::uint32_t left, std::uint32_t right, std::uint32_t buffer, bool blocking) { const std::uint32_t channel = ctx.gpr[4]; if (channel >= 8u) { ctx.set_gpr(2, 0x80260003u); return; } auto &state = audio_channels[channel]; if (!state.reserved) { ctx.set_gpr(2, 0x80260001u); return; } if (!blocking && audio_remaining_samples(state) != 0u) { ctx.set_gpr(2, 0x80260002u); return; } const bool stereo = state.format == 0u; const std::uint32_t frames = state.sample_count; const std::size_t samples = static_cast(frames) * (stereo ? 2u : 1u); const std::uint64_t start_us = audio_queue_buffer(state, frames); if (buffer != 0u && frames != 0u && rt.memory().contains(buffer, samples * sizeof(std::int16_t))) { std::vector pcm(samples); for (std::size_t index = 0; index < samples; ++index) { pcm[index] = static_cast( rt.memory().load16(buffer + static_cast(index * 2u))); } audio_output_submit(pcm, frames, stereo, left, right, 44100u, channel, start_us, virtual_time_us); } const std::uint64_t wait_us = start_us > virtual_time_us ? start_us - virtual_time_us : 0u; if (blocking) { (void)delay_current_thread(rt, ctx, static_cast(wait_us), frames); } else { ctx.set_gpr(2, frames); } }; runtime.register_hle("sceAudio", 0x136CAF51u, [audio_output_common](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { audio_output_common(rt, ctx, ctx.gpr[5], ctx.gpr[5], ctx.gpr[6], true); }); runtime.register_hle("sceAudio", 0x8C1009B2u, [audio_output_common](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { audio_output_common(rt, ctx, ctx.gpr[5], ctx.gpr[5], ctx.gpr[6], false); }); runtime.register_hle("sceAudio", 0xE2D56B2Du, [audio_output_common](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { audio_output_common(rt, ctx, ctx.gpr[5], ctx.gpr[6], ctx.gpr[7], false); }); runtime.register_hle("sceAudio", 0x13F592BCu, [audio_output_common](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { audio_output_common(rt, ctx, ctx.gpr[5], ctx.gpr[6], ctx.gpr[7], true); }); runtime.register_hle("sceMpeg", 0x42560F23u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); if (state == mpeg_contexts.end()) { ctx.set_gpr(2, 0x806101FEu); return; } const std::uint32_t stream_id = next_mpeg_stream_id++; state->second.streams.emplace(stream_id, MpegStreamState{ctx.gpr[5], ctx.gpr[6]}); ctx.set_gpr(2, stream_id); }); runtime.register_hle("sceMpeg", 0x591A4AA2u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); if (state == mpeg_contexts.end() || state->second.streams.erase(ctx.gpr[5]) != 1u) { ctx.set_gpr(2, 0x806101FEu); return; } set_success(ctx); }); runtime.register_hle("sceMpeg", 0xA780CF7Eu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); if (state == mpeg_contexts.end()) { ctx.set_gpr(2, 0x806101FEu); return; } for (std::size_t index = 0; index < state->second.avc_es_buffers.size(); ++index) { if (!state->second.avc_es_buffers[index]) { state->second.avc_es_buffers[index] = true; ctx.set_gpr(2, static_cast(index + 1u)); return; } } ctx.set_gpr(2, 0u); }); runtime.register_hle("sceMpeg", 0xCEB870B1u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); const std::uint32_t buffer = ctx.gpr[5]; if (state == mpeg_contexts.end() || buffer == 0u || buffer > 2u || !state->second.avc_es_buffers[buffer - 1u]) { ctx.set_gpr(2, 0x806101FEu); return; } state->second.avc_es_buffers[buffer - 1u] = false; set_success(ctx); }); runtime.register_hle("sceMpeg", 0xF8DCB679u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { if (mpeg_contexts.find(ctx.gpr[4]) == mpeg_contexts.end() || !rt.memory().contains(ctx.gpr[5], 4u) || !rt.memory().contains(ctx.gpr[6], 4u)) { ctx.set_gpr(2, 0x80610103u); return; } rt.memory().store32(ctx.gpr[5], 2112u); rt.memory().store32(ctx.gpr[6], 8192u); set_success(ctx); }); runtime.register_hle("sceMpeg", 0x167AFD9Eu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t au = ctx.gpr[6]; if (mpeg_contexts.find(ctx.gpr[4]) == mpeg_contexts.end() || !rt.memory().contains(au, 24u)) { ctx.set_gpr(2, 0x80610103u); return; } rt.memory().zero(au, 24u); rt.memory().store32(au + 8u, 0xFFFFFFFFu); rt.memory().store32(au + 12u, 0xFFFFFFFFu); set_success(ctx); }); runtime.register_hle("sceMpeg", 0xFE246728u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); const std::uint32_t au = ctx.gpr[6]; const std::uint32_t attributes = ctx.gpr[7]; if (state == mpeg_contexts.end() || !rt.memory().contains(au, 24u)) { ctx.set_gpr(2, 0x806101FEu); return; } if (state->second.video_au_count >= mpeg_au_limit(state->second)) { write_mpeg_timestamp(rt.memory(), au, 0u); write_mpeg_timestamp(rt.memory(), au + 8u, 0u); ctx.set_gpr(2, 0x80618001u); return; } const std::uint64_t pts = static_cast(state->second.video_au_count) * 3003u; const std::uint64_t dts = pts >= 3003u ? pts - 3003u : 0u; write_mpeg_timestamp(rt.memory(), au, pts); write_mpeg_timestamp(rt.memory(), au + 8u, dts); rt.memory().store32(au + 16u, ctx.gpr[5]); rt.memory().store32(au + 20u, 2048u); if (attributes != 0u && rt.memory().contains(attributes, 4u)) rt.memory().store32(attributes, 1u); ++state->second.video_au_count; set_success(ctx); }); runtime.register_hle("sceMpeg", 0xE1CE83A7u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); const std::uint32_t au = ctx.gpr[6]; if (state == mpeg_contexts.end() || !rt.memory().contains(au, 24u)) { ctx.set_gpr(2, 0x806101FEu); return; } if (state->second.audio_au_count >= mpeg_au_limit(state->second)) { write_mpeg_timestamp(rt.memory(), au, 0u); write_mpeg_timestamp(rt.memory(), au + 8u, 0u); ctx.set_gpr(2, 0x80618001u); return; } const std::uint64_t pts = static_cast(state->second.audio_au_count) * 4180u; write_mpeg_timestamp(rt.memory(), au, pts); write_mpeg_timestamp(rt.memory(), au + 8u, pts); rt.memory().store32(au + 16u, ctx.gpr[5]); rt.memory().store32(au + 20u, 2048u); ++state->second.audio_au_count; set_success(ctx); }); runtime.register_hle("sceMpeg", 0x707B7629u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); if (state == mpeg_contexts.end()) { ctx.set_gpr(2, 0x806101FEu); return; } set_success(ctx); }); runtime.register_hle("sceMpeg", 0x0E3C2E9Du, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); const std::uint32_t au = ctx.gpr[5]; std::uint32_t frame_width = ctx.gpr[6]; const std::uint32_t buffer_pointer = ctx.gpr[7]; const std::uint32_t status_pointer = ctx.gpr[8]; if (state == mpeg_contexts.end() || !state->second.analyzed || !rt.memory().contains(au, 24u) || !rt.memory().contains(buffer_pointer, 4u) || !rt.memory().contains(status_pointer, 4u)) { ctx.set_gpr(2, 0x80610103u); return; } const ParsedPsmfHeader &header = state->second.header; if (frame_width == 0u) frame_width = header.width; if (header.width == 0u || header.height == 0u || frame_width < header.width) { ctx.set_gpr(2, 0x806201FEu); return; } const std::uint32_t destination = rt.memory().load32(buffer_pointer); const std::size_t source_stride = static_cast(header.width) * 4u; const std::size_t destination_stride = static_cast(frame_width) * 4u; const std::size_t destination_bytes = destination_stride * header.height; if (destination == 0u || !rt.memory().contains(destination, destination_bytes)) { ctx.set_gpr(2, 0x80610103u); return; } std::vector frame(source_stride * header.height); if (!read_video_frame(state->second, frame)) { rt.memory().store32(status_pointer, 0u); ctx.set_gpr(2, 0x80628002u); return; } for (std::uint32_t y = 0u; y < header.height; ++y) { rt.memory().copy_in( destination + static_cast(y * destination_stride), std::span(frame.data() + y * source_stride, source_stride)); } rt.memory().store32(status_pointer, 1u); if (std::getenv("LCS_MPEG_DIAG") != nullptr) { static std::uint32_t decoded_frames = 0u; if ((++decoded_frames % 30u) == 1u) { std::uint64_t non_black = 0u; for (std::size_t i = 0; i + 3 < frame.size(); i += 4) { if ((frame[i] | frame[i + 1] | frame[i + 2]) != 0u) ++non_black; } std::uint64_t display_non_black = 0u; const std::uint32_t display_base = display_state.frame_buffer; if (display_base != 0u) { for (std::uint32_t y = 0; y < 272u; ++y) { for (std::uint32_t x = 0; x < 480u; ++x) { const std::uint32_t address = display_base + (y * 512u + x) * 4u; if (!rt.memory().contains(address, 4u)) continue; if ((rt.memory().load32(address) & 0x00FFFFFFu) != 0u) ++display_non_black; } } } std::cerr << "[mpeg] frame #" << decoded_frames << " -> " << psprecomp::hex32(destination) << " non_black=" << non_black << "/" << (frame.size() / 4u) << " (display fb " << psprecomp::hex32(display_base) << " non_black=" << display_non_black << ")\n"; } } set_success(ctx); }); runtime.register_hle("sceMpeg", 0x740FCCD1u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); const std::uint32_t buffer_pointer = ctx.gpr[6]; const std::uint32_t status_pointer = ctx.gpr[7]; if (state == mpeg_contexts.end()) { ctx.set_gpr(2, 0x806101FEu); return; } if (!rt.memory().contains(buffer_pointer, 4u) || !rt.memory().contains(status_pointer, 4u)) { ctx.set_gpr(2, 0x80610103u); return; } rt.memory().store32(status_pointer, 0u); set_success(ctx); }); runtime.register_hle("sceMpeg", 0xB240A59Eu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t ring = ctx.gpr[4]; std::int32_t requested = static_cast(ctx.gpr[5]); const std::int32_t caller_available = static_cast(ctx.gpr[6]); if (!rt.memory().contains(ring, 48u)) { ctx.set_gpr(2, 0x80610103u); return; } const std::int32_t packets = static_cast(rt.memory().load32(ring)); const std::int32_t used = static_cast(rt.memory().load32(ring + 12u)); const std::int32_t write_position = static_cast(rt.memory().load32(ring + 8u)); const std::uint32_t data = rt.memory().load32(ring + 20u); const std::uint32_t callback = rt.memory().load32(ring + 24u); const std::uint32_t callback_argument = rt.memory().load32(ring + 28u); if (packets <= 0 || callback == 0u) { ctx.set_gpr(2, 0x806101FEu); return; } requested = std::min({requested, caller_available, std::max(0, packets - used)}); if (requested <= 0) { ctx.set_gpr(2, 0u); return; } const std::int32_t desired = std::min(requested, packets - write_position); if (desired <= 0) { ctx.set_gpr(2, 0u); return; } psprecomp::AllegrexContext resume = ctx; resume.pc = ctx.gpr[31]; resume.set_gpr(2, 0u); auto &frames = async_return_frames[thread_table.current_uid]; if (!frames.empty()) { ctx.set_gpr(2, 0u); return; } frames.push_back(AsyncReturnFrame{resume, 0, AsyncReturnKind::MpegRingbuffer, ring}); ctx.set_gpr(4, data + static_cast(write_position) * 2048u); ctx.set_gpr(5, static_cast(desired)); ctx.set_gpr(6, callback_argument); ctx.set_gpr(31, 0x00000004u); ctx.pc = callback; if (std::getenv("LCS_MPEG_DIAG") != nullptr) { std::cerr << "[mpeg] ring put callback=" << psprecomp::hex32(callback) << " desired=" << desired << "\n"; } }); runtime.register_hle("sceMpeg", 0xB5F6DC87u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t ring = ctx.gpr[4]; if (!rt.memory().contains(ring, 48u)) { ctx.set_gpr(2, 0x800200D3u); return; } const std::int32_t packets = static_cast(rt.memory().load32(ring)); const std::int32_t used = static_cast(rt.memory().load32(ring + 12u)); ctx.set_gpr(2, static_cast(std::max(0, packets - used))); }); runtime.register_hle("sceMpeg", 0x800C44DFu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto state = mpeg_contexts.find(ctx.gpr[4]); const std::uint32_t au = ctx.gpr[5]; const std::uint32_t output = ctx.gpr[6]; if (state == mpeg_contexts.end() || !rt.memory().contains(au, 24u) || !rt.memory().contains(output, 8192u)) { ctx.set_gpr(2, 0x80610103u); return; } MpegContextState &mpeg = state->second; if (!mpeg.source_path.empty() && (!mpeg.audio.is_open() || mpeg.audio_source != mpeg.source_path)) { mpeg.audio_source = mpeg.source_path; (void)mpeg.audio.open(mpeg.source_path); } std::array pcm{}; const std::size_t decoded = mpeg.audio.is_open() ? mpeg.audio.read(pcm) : 0u; if (decoded == 0u) rt.memory().zero(output, 8192u); else rt.memory().copy_in(output, std::span(pcm.data(), pcm.size())); set_success(ctx); }); runtime.register_hle("ModuleMgrForUser", 0xB7F46618u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto fd = static_cast(ctx.gpr[4]); if (!file_table.files.contains(fd)) { ctx.set_gpr(2, 0x80010009u); return; } const std::int32_t uid = next_module_uid++; loaded_modules.emplace(uid, false); ctx.set_gpr(2, static_cast(uid)); }); runtime.register_hle("ModuleMgrForUser", 0x50F0C1ECu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto found = loaded_modules.find(uid); if (found == loaded_modules.end()) { ctx.set_gpr(2, 0x8002012Eu); return; } const std::uint32_t status = ctx.gpr[7]; if (status != 0u && rt.memory().contains(status, 4u)) rt.memory().store32(status, 0u); found->second = true; ctx.set_gpr(2, static_cast(uid)); }); runtime.register_hle("ModuleMgrForUser", 0xD1FF982Au, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); const auto found = loaded_modules.find(uid); if (found == loaded_modules.end()) { ctx.set_gpr(2, 0x8002012Eu); return; } const std::uint32_t status = ctx.gpr[7]; if (status != 0u && rt.memory().contains(status, 4u)) rt.memory().store32(status, 0u); found->second = false; ctx.set_gpr(2, 0u); }); runtime.register_hle("ModuleMgrForUser", 0x2E0911AAu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { const auto uid = static_cast(ctx.gpr[4]); ctx.set_gpr(2, loaded_modules.erase(uid) == 1u ? 0u : 0x8002012Eu); }); runtime.register_hle("scePower", 0x04B7766Eu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("scePower", 0xDFA8BAF8u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("sceUmdUser", 0xAEE7404Du, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("sceUmdUser", 0xBD2BDE07u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("sceUmdUser", 0x46EBB729u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, 1u); }); runtime.register_hle("sceUmdUser", 0x6B4A146Cu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, 0x32u); }); runtime.register_hle("sceUmdUser", 0x8EF08FCEu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("sceUmdUser", 0xC6183D47u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { umd_activated = true; notify_umd_callback(); set_success(ctx); }); runtime.register_hle("sceWlanDrv", 0xD7763699u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, 0u); }); runtime.register_hle("UtilsForUser", 0x27CC57F0u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto seconds = static_cast(std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count()); if (ctx.gpr[4] != 0u && rt.memory().contains(ctx.gpr[4], 4u)) rt.memory().store32(ctx.gpr[4], seconds); ctx.set_gpr(2, seconds); }); runtime.register_hle("UtilsForUser", 0x71EC4271u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto micros = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); if (ctx.gpr[4] != 0u && rt.memory().contains(ctx.gpr[4], 8u)) { rt.memory().store32(ctx.gpr[4], static_cast(micros / 1000000)); rt.memory().store32(ctx.gpr[4] + 4u, static_cast(micros % 1000000)); } if (ctx.gpr[5] != 0u && rt.memory().contains(ctx.gpr[5], 8u)) rt.memory().zero(ctx.gpr[5], 8u); set_success(ctx); }); runtime.register_hle("UtilsForUser", 0x91E4F6A7u, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { ctx.set_gpr(2, static_cast(virtual_time_us)); }); runtime.register_hle("SysMemUserForUser", 0x13A5ABEFu, [](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) { set_success(ctx); }); runtime.register_hle("sceRtc", 0x6FF40ACCu, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const std::uint32_t date = ctx.gpr[4]; const std::uint32_t out = ctx.gpr[5]; if (!rt.memory().contains(date, 16u) || !rt.memory().contains(out, 8u)) { ctx.set_gpr(2, 0x80000103u); return; } std::int64_t year = rt.memory().load16(date + 0u); const std::uint32_t month_raw = rt.memory().load16(date + 2u); const std::uint32_t day_raw = rt.memory().load16(date + 4u); const unsigned month = month_raw == 0u ? 1u : month_raw; const unsigned day = day_raw == 0u ? 1u : day_raw; const std::uint64_t hour = rt.memory().load16(date + 6u); const std::uint64_t minute = rt.memory().load16(date + 8u); const std::uint64_t second = rt.memory().load16(date + 10u); const std::uint64_t microsecond = rt.memory().load32(date + 12u); year -= month <= 2u; const std::int64_t era = (year >= 0 ? year : year - 399) / 400; const unsigned yoe = static_cast(year - era * 400); const unsigned doy = (153u * (month + (month > 2u ? -3 : 9)) + 2u) / 5u + day - 1u; const unsigned doe = yoe * 365u + yoe / 4u - yoe / 100u + doy; constexpr std::int64_t kDaysToUnixEpoch = 719162; const std::int64_t days = era * 146097 + static_cast(doe) - 719468 + kDaysToUnixEpoch; const std::uint64_t tick = static_cast(days) * 86400000000ull + (hour * 3600ull + minute * 60ull + second) * 1000000ull + microsecond; rt.memory().store32(out, static_cast(tick)); rt.memory().store32(out + 4u, static_cast(tick >> 32u)); set_success(ctx); }); runtime.register_hle("sceRtc", 0x9ED0AE87u, [](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) { const auto load = [&](std::uint32_t address) { return static_cast(rt.memory().load32(address)) | (static_cast(rt.memory().load32(address + 4u)) << 32u); }; const std::uint64_t first = load(ctx.gpr[4]); const std::uint64_t second = load(ctx.gpr[5]); ctx.set_gpr(2, first < second ? 0xFFFFFFFFu : (first > second ? 1u : 0u)); }); } void debug_dump_threads() { std::cerr << "[lcs-debug] thread_table.current_uid=" << thread_table.current_uid << " threads=" << thread_table.threads.size() << " continuations=" << thread_table.continuations.size() << " virtual_time_us=" << virtual_time_us << "\n"; for (const auto &[uid, thread] : thread_table.threads) { std::cerr << "[lcs-debug] thread uid=" << uid << " name=\"" << thread.name << "\" state=" << static_cast(thread.state) << " priority=" << thread.priority << " delay_until_us=" << thread.delay_until_us << " entry=" << psprecomp::hex32(thread.entry) << " suspended_pc=" << psprecomp::hex32(thread.suspended_context.pc) << "\n"; } for (const auto &continuation : thread_table.continuations) { std::cerr << "[lcs-debug] ready uid=" << continuation.uid << "\n"; } std::cerr << "[lcs-debug] semaphores=" << semaphore_table.semaphores.size() << " event_flags=" << event_flag_table.flags.size() << " partitions=" << partition_table.blocks.size() << "\n"; for (const auto &[uid, sema] : semaphore_table.semaphores) { std::cerr << "[lcs-debug] sema uid=" << uid << " name=\"" << sema.name << "\" count=" << sema.count << " max=" << sema.maximum << " waiters=" << sema.waiters.size() << "\n"; } for (const auto &[uid, flag] : event_flag_table.flags) { std::cerr << "[lcs-debug] eventflag uid=" << uid << " name=\"" << flag.name << "\" pattern=" << psprecomp::hex32(flag.current_pattern) << " waiters=" << flag.waiters.size() << "\n"; } } }