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