Merge remote-tracking branch 'upstream/main' into fix/ampr-wait-signatures
# Conflicts: # docs/dev/TechnicalDebt.md
This commit is contained in:
commit
e3f18eec00
|
|
@ -32,4 +32,14 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(audio_out2_pad_mix PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
|
||||
add_test_executable(audio_out_mix_level_pad_spk_tests tests/AudioOutMixLevelPadSpk.cpp)
|
||||
target_include_directories(audio_out_mix_level_pad_spk_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(audio_out_mix_level_pad_spk_tests PRIVATE libSceAudioOut libc)
|
||||
configure_windows_unwind(audio_out_mix_level_pad_spk_tests)
|
||||
add_test(NAME audio_out_mix_level_pad_spk COMMAND audio_out_mix_level_pad_spk_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(audio_out_mix_level_pad_spk PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAudioOut>")
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ static constexpr int PORT_TYPE_AUX = 127;
|
|||
|
||||
static constexpr int PORTS_MAX = 32;
|
||||
static constexpr int DEFAULT_VOLUME = 32768;
|
||||
static constexpr int DEFAULT_PADSPK_MIX_LEVEL = 11626;
|
||||
static constexpr std::uint32_t FORMAT_MASK = 0xFFu;
|
||||
static constexpr std::uint64_t TARGET_LATENCY_US = 40000;
|
||||
static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000;
|
||||
|
|
@ -86,6 +87,7 @@ struct Port {
|
|||
Format format = Format::Unknown;
|
||||
int channels = 0;
|
||||
int volume[8] = {};
|
||||
int mixLevel = DEFAULT_VOLUME;
|
||||
std::uint64_t lastOutputTime = 0;
|
||||
SDL_AudioDeviceID device = 0;
|
||||
SDL_AudioSpec spec = {};
|
||||
|
|
@ -143,7 +145,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector
|
|||
const auto bps = bytesPerSample(port.format);
|
||||
const auto size = frames * ch * bps;
|
||||
|
||||
bool volumeChanged = false;
|
||||
bool volumeChanged = port.mixLevel != DEFAULT_VOLUME;
|
||||
for (std::uint32_t i = 0; i < ch; i++) {
|
||||
if (port.volume[i] != DEFAULT_VOLUME) {
|
||||
volumeChanged = true;
|
||||
|
|
@ -161,11 +163,12 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector
|
|||
if (formatIsFloat(port.format)) {
|
||||
auto* dst = reinterpret_cast<float*>(buf.data());
|
||||
const auto* src = static_cast<const float*>(data);
|
||||
const float mixLevel = static_cast<float>(port.mixLevel) / static_cast<float>(DEFAULT_VOLUME);
|
||||
for (std::uint32_t fr = 0; fr < frames; fr++) {
|
||||
for (std::uint32_t c = 0; c < ch; c++) {
|
||||
const auto srcCh = isStd ? STD_8CH_MAP[c] : c;
|
||||
dst[fr * ch + c] = src[fr * ch + srcCh] *
|
||||
(static_cast<float>(port.volume[c]) / static_cast<float>(DEFAULT_VOLUME));
|
||||
(static_cast<float>(port.volume[c]) / static_cast<float>(DEFAULT_VOLUME)) * mixLevel;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -175,7 +178,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector
|
|||
for (std::uint32_t c = 0; c < ch; c++) {
|
||||
const auto srcCh = isStd ? STD_8CH_MAP[c] : c;
|
||||
std::int64_t s = static_cast<std::int64_t>(src[fr * ch + srcCh]) *
|
||||
port.volume[c] / DEFAULT_VOLUME;
|
||||
port.volume[c] * port.mixLevel / (static_cast<std::int64_t>(DEFAULT_VOLUME) * DEFAULT_VOLUME);
|
||||
s = std::clamp(s,
|
||||
static_cast<std::int64_t>(std::numeric_limits<std::int16_t>::min()),
|
||||
static_cast<std::int64_t>(std::numeric_limits<std::int16_t>::max()));
|
||||
|
|
@ -325,6 +328,7 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len
|
|||
port.format = format;
|
||||
port.channels = channelsForFormat(format);
|
||||
port.lastOutputTime = 0;
|
||||
port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME;
|
||||
for (int c = 0; c < port.channels; c++) {
|
||||
port.volume[c] = DEFAULT_VOLUME;
|
||||
}
|
||||
|
|
@ -494,9 +498,23 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetMixLevelPadSpk(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
Port* port = getPort(handle);
|
||||
if (port == nullptr) {
|
||||
return -2144993277;
|
||||
}
|
||||
if (port->type != PORT_TYPE_PADSPK) {
|
||||
return -2144993270;
|
||||
}
|
||||
if (mixLevel > DEFAULT_VOLUME) {
|
||||
return -2144993260;
|
||||
}
|
||||
if (mixLevel < 0) {
|
||||
throw std::runtime_error("sceAudioOutSetMixLevelPadSpk: negative mix level not supported");
|
||||
}
|
||||
port->mixLevel = mixLevel;
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceAudioOutClose(int);
|
||||
int APS5_VABI sceAudioOutOutput(int, const void*);
|
||||
int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*);
|
||||
int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, int);
|
||||
}
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
static void SetEnvironment(const char* name, const std::string& value) {
|
||||
#ifdef _WIN32
|
||||
_putenv_s(name, value.c_str());
|
||||
#else
|
||||
setenv(name, value.c_str(), 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
template<typename TFunction>
|
||||
static bool ThrowsRuntimeError(TFunction function) {
|
||||
try {
|
||||
function();
|
||||
} catch (const std::runtime_error&) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int user = 0x10000000;
|
||||
constexpr int portTypeMain = 0;
|
||||
constexpr int portTypePadSpeaker = 4;
|
||||
constexpr std::uint32_t formatS16Mono = 0;
|
||||
constexpr std::uint32_t formatFloatStereo = 4;
|
||||
constexpr std::uint32_t frames = 256;
|
||||
constexpr std::uint32_t frequency = 48000;
|
||||
constexpr int unity = 32768;
|
||||
constexpr int defaultMixLevel = 11626;
|
||||
constexpr int invalidPort = static_cast<int>(0x80260003);
|
||||
constexpr int invalidPortType = static_cast<int>(0x8026000A);
|
||||
constexpr int invalidMixLevel = static_cast<int>(0x80260014);
|
||||
|
||||
template<typename TSample, typename TSetup>
|
||||
std::vector<TSample> Play(int type, std::uint32_t format, const std::vector<TSample>& block, TSetup setup) {
|
||||
const auto path = std::filesystem::temp_directory_path() / "anyps5_audio_out_mix_level_pad_spk.raw";
|
||||
std::filesystem::remove(path);
|
||||
SetEnvironment("SDL_DISKAUDIOFILE", path.string());
|
||||
|
||||
const int handle = sceAudioOutOpen(user, type, 0, frames, frequency, format);
|
||||
Require(handle > 0);
|
||||
setup(handle);
|
||||
Require(sceAudioOutOutput(handle, block.data()) == static_cast<int>(frames));
|
||||
Require(sceAudioOutOutput(handle, nullptr) == static_cast<int>(frames));
|
||||
Require(sceAudioOutClose(handle) == 0);
|
||||
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
const std::vector<char> bytes{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||
file.close();
|
||||
std::filesystem::remove(path);
|
||||
Require(bytes.size() >= block.size() * sizeof(TSample) && bytes.size() % sizeof(TSample) == 0);
|
||||
|
||||
const auto* samples = reinterpret_cast<const TSample*>(bytes.data());
|
||||
std::size_t first = 0;
|
||||
std::size_t last = bytes.size() / sizeof(TSample);
|
||||
while (first < last && samples[first] == TSample{}) first++;
|
||||
while (last > first && samples[last - 1] == TSample{}) last--;
|
||||
return {samples + first, samples + last};
|
||||
}
|
||||
|
||||
std::vector<std::int16_t> Ramp() {
|
||||
std::vector<std::int16_t> block(frames);
|
||||
for (std::uint32_t frame = 0; frame < frames; frame++) {
|
||||
block[frame] = static_cast<std::int16_t>((frame % 2 == 0 ? 1 : -1) * 256 * (static_cast<int>(frame % 100) + 1));
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
std::vector<std::int16_t> Scaled(const std::vector<std::int16_t>& block, int level) {
|
||||
std::vector<std::int16_t> scaled;
|
||||
for (const std::int16_t sample : block) {
|
||||
Require(sample * level % unity == 0);
|
||||
scaled.push_back(static_cast<std::int16_t>(sample * level / unity));
|
||||
}
|
||||
return scaled;
|
||||
}
|
||||
|
||||
void TestDefaultLevel() {
|
||||
std::vector<std::int16_t> block(frames);
|
||||
for (std::uint32_t frame = 0; frame < frames; frame++) block[frame] = frame % 2 == 0 ? 16384 : -16384;
|
||||
const auto played = Play(portTypePadSpeaker, formatS16Mono, block, [](int) {});
|
||||
Require(played == Scaled(block, defaultMixLevel));
|
||||
Require(played[0] == 5813 && played[1] == -5813);
|
||||
}
|
||||
|
||||
void TestLevels() {
|
||||
const auto block = Ramp();
|
||||
Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) {
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity) == 0);
|
||||
}) == block);
|
||||
Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) {
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0);
|
||||
}) == Scaled(block, unity / 2));
|
||||
Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) {
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity) == 0);
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 4) == 0);
|
||||
}) == Scaled(block, unity / 4));
|
||||
Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) {
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, 0) == 0);
|
||||
}).empty());
|
||||
}
|
||||
|
||||
void TestLevelWithVolume() {
|
||||
const auto block = Ramp();
|
||||
Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) {
|
||||
int volume = unity / 2;
|
||||
Require(sceAudioOutSetVolume(handle, 1, &volume) == 0);
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0);
|
||||
}) == Scaled(block, unity / 4));
|
||||
|
||||
std::vector<float> stereo(frames * 2);
|
||||
for (std::uint32_t frame = 0; frame < frames; frame++) {
|
||||
stereo[frame * 2] = (frame % 2 == 0 ? 0.5f : -0.25f);
|
||||
stereo[frame * 2 + 1] = (frame % 2 == 0 ? -1.0f : 0.125f);
|
||||
}
|
||||
const auto played = Play(portTypePadSpeaker, formatFloatStereo, stereo, [](int handle) {
|
||||
int volume[2] = {unity, unity / 2};
|
||||
Require(sceAudioOutSetVolume(handle, 2, volume) == 0);
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 4) == 0);
|
||||
});
|
||||
Require(played.size() == stereo.size());
|
||||
for (std::uint32_t frame = 0; frame < frames; frame++) {
|
||||
Require(played[frame * 2] == stereo[frame * 2] * 0.25f);
|
||||
Require(played[frame * 2 + 1] == stereo[frame * 2 + 1] * 0.125f);
|
||||
}
|
||||
}
|
||||
|
||||
void TestOtherPortType() {
|
||||
const auto block = Ramp();
|
||||
Require(Play(portTypeMain, formatS16Mono, block, [](int) {}) == block);
|
||||
Require(Play(portTypeMain, formatS16Mono, block, [](int handle) {
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == invalidPortType);
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidPortType);
|
||||
}) == block);
|
||||
}
|
||||
|
||||
void TestRejectedLevels() {
|
||||
const auto block = Ramp();
|
||||
int closed = 0;
|
||||
Require(Play(portTypePadSpeaker, formatS16Mono, block, [&closed](int handle) {
|
||||
closed = handle;
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0);
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel);
|
||||
Require(ThrowsRuntimeError([handle] { sceAudioOutSetMixLevelPadSpk(handle, -1); }));
|
||||
Require(sceAudioOutSetMixLevelPadSpk(0, unity) == invalidPort);
|
||||
Require(sceAudioOutSetMixLevelPadSpk(handle + 1, unity) == invalidPort);
|
||||
}) == Scaled(block, unity / 2));
|
||||
Require(sceAudioOutSetMixLevelPadSpk(closed, unity) == invalidPort);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
SetEnvironment("SDL_AUDIODRIVER", "disk");
|
||||
TestDefaultLevel();
|
||||
TestLevels();
|
||||
TestLevelWithVolume();
|
||||
TestOtherPortType();
|
||||
TestRejectedLevels();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
add_library(libSceErrorDialog SHARED EXCLUDE_FROM_ALL Export.cpp)
|
||||
target_include_directories(libSceErrorDialog PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
configure_windows_unwind(libSceErrorDialog)
|
||||
|
||||
if(MINGW)
|
||||
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
|
||||
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_error_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestErrorDialog.cpp)
|
||||
target_include_directories(guest_error_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_error_dialog_tests PRIVATE libSceErrorDialog)
|
||||
add_test(NAME guest_error_dialog COMMAND guest_error_dialog_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_error_dialog PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceErrorDialog>")
|
||||
endif()
|
||||
endif()
|
||||
|
|
@ -1,39 +1,95 @@
|
|||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include "SceTypes.hpp"
|
||||
#include "HitLog.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr int kStatusNone = 0;
|
||||
constexpr int kStatusInitialized = 1;
|
||||
constexpr int kStatusRunning = 2;
|
||||
constexpr int kStatusFinished = 3;
|
||||
|
||||
constexpr int kErrOk = 0;
|
||||
constexpr int kErrNotInitialized = static_cast<int>(0x80ED0001u);
|
||||
constexpr int kErrAlreadyInitialized = static_cast<int>(0x80ED0002u);
|
||||
constexpr int kErrParam = static_cast<int>(0x80ED0003u);
|
||||
constexpr int kErrInvalidState = static_cast<int>(0x80ED0005u);
|
||||
|
||||
constexpr std::size_t kParamSize = 16;
|
||||
|
||||
std::mutex g_lock;
|
||||
int g_status = kStatusNone;
|
||||
|
||||
bool Readable(const void* p, std::size_t n) {
|
||||
#ifdef _WIN32
|
||||
if (p == nullptr) return false;
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (VirtualQuery(p, &mbi, sizeof(mbi)) == 0 || mbi.State != MEM_COMMIT) return false;
|
||||
if ((mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) return false;
|
||||
const auto end = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
|
||||
return reinterpret_cast<std::uintptr_t>(p) + n <= end;
|
||||
#else
|
||||
return p != nullptr;
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceErrorDialogClose(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceErrorDialogGetStatus(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceErrorDialogInitialize(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
std::lock_guard g(g_lock);
|
||||
if (g_status != kStatusNone) return kErrAlreadyInitialized;
|
||||
g_status = kStatusInitialized;
|
||||
return kErrOk;
|
||||
}
|
||||
|
||||
int APS5_VABI sceErrorDialogOpen(const void* param) {
|
||||
(void)param;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceErrorDialogTerminate(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
std::lock_guard g(g_lock);
|
||||
if (g_status != kStatusInitialized && g_status != kStatusFinished) return kErrInvalidState;
|
||||
if (!Readable(param, kParamSize)) return kErrParam;
|
||||
std::int32_t size = 0;
|
||||
std::memcpy(&size, param, sizeof(size));
|
||||
if (static_cast<std::size_t>(size) != kParamSize) return kErrParam;
|
||||
std::int32_t errorCode = 0;
|
||||
std::int32_t userId = 0;
|
||||
std::memcpy(&errorCode, static_cast<const std::uint8_t*>(param) + 4, sizeof(errorCode));
|
||||
std::memcpy(&userId, static_cast<const std::uint8_t*>(param) + 8, sizeof(userId));
|
||||
APS5_HIT("ERRORDIALOG", "open: error_code=0x%08X user_id=%d", errorCode, userId);
|
||||
g_status = kStatusRunning;
|
||||
return kErrOk;
|
||||
}
|
||||
|
||||
int APS5_VABI sceErrorDialogUpdateStatus(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
std::lock_guard g(g_lock);
|
||||
if (g_status == kStatusRunning) g_status = kStatusFinished;
|
||||
return g_status;
|
||||
}
|
||||
|
||||
int APS5_VABI sceErrorDialogGetStatus(void) {
|
||||
std::lock_guard g(g_lock);
|
||||
return g_status;
|
||||
}
|
||||
|
||||
int APS5_VABI sceErrorDialogClose(void) {
|
||||
std::lock_guard g(g_lock);
|
||||
if (g_status != kStatusRunning) return kErrInvalidState;
|
||||
g_status = kStatusFinished;
|
||||
return kErrOk;
|
||||
}
|
||||
|
||||
int APS5_VABI sceErrorDialogTerminate(void) {
|
||||
std::lock_guard g(g_lock);
|
||||
if (g_status == kStatusNone) return kErrNotInitialized;
|
||||
g_status = kStatusNone;
|
||||
return kErrOk;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,10 +171,9 @@ int APS5_VABI scePadResetOrientation(int handle) {
|
|||
}
|
||||
|
||||
int APS5_VABI scePadSetAngularVelocityDeadbandState(int handle, bool enable) {
|
||||
(void)handle;
|
||||
(void)enable;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE;
|
||||
if (enable) NotImplemented_nid_no_patch(__func__);
|
||||
return PAD_OK;
|
||||
}
|
||||
|
||||
int APS5_VABI scePadSetLightBar(int handle, const PadLightBarParam* param) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ endif()
|
|||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_rtc_tests ${LIBS_INCLUDE_DIR}/tests/GuestRtc.cpp)
|
||||
target_include_directories(guest_rtc_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_rtc_tests PRIVATE libSceRtc)
|
||||
target_link_libraries(guest_rtc_tests PRIVATE libSceRtc libc)
|
||||
configure_windows_unwind(guest_rtc_tests)
|
||||
add_test(NAME guest_rtc COMMAND guest_rtc_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_rtc PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
|
|
|
|||
|
|
@ -139,6 +139,23 @@ bool parseDigits(const char*& cursor, int count, int& value) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool parseZoneSuffix(const char*& cursor, std::int64_t& offsetMinutes) {
|
||||
offsetMinutes = 0;
|
||||
if (*cursor == 'Z' || *cursor == 'z') {
|
||||
++cursor;
|
||||
return true;
|
||||
}
|
||||
if (*cursor == '+' || *cursor == '-') {
|
||||
const int sign = *cursor++ == '-' ? -1 : 1;
|
||||
int offsetHours = 0, offsetMinute = 0;
|
||||
if (!parseDigits(cursor, 2, offsetHours) || *cursor++ != ':' || !parseDigits(cursor, 2, offsetMinute)) return false;
|
||||
if (offsetHours > 23 || offsetMinute > 59) return false;
|
||||
offsetMinutes = sign * (offsetHours * 60 + offsetMinute);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
|
@ -282,18 +299,37 @@ int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) {
|
|||
for (; *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast<std::uint32_t>(*cursor - '0') * scale;
|
||||
}
|
||||
std::int64_t offsetMinutes = 0;
|
||||
if (*cursor == 'Z' || *cursor == 'z') {
|
||||
if (!parseZoneSuffix(cursor, offsetMinutes)) return SCE_RTC_ERROR_BAD_PARSE;
|
||||
if (*cursor != 0) return SCE_RTC_ERROR_BAD_PARSE;
|
||||
const RtcDateTime time{static_cast<std::uint16_t>(year), static_cast<std::uint16_t>(month), static_cast<std::uint16_t>(day),
|
||||
static_cast<std::uint16_t>(hour), static_cast<std::uint16_t>(minute), static_cast<std::uint16_t>(second), microsecond};
|
||||
if (const int result = validate(&time); result != 0) return result;
|
||||
const RtcTick local{toTick(time)};
|
||||
return addTicks(utc, &local, -offsetMinutes, TICKS_PER_MINUTE);
|
||||
}
|
||||
|
||||
int APS5_VABI sceRtcParseDateTime(RtcTick* utc, const char* date_time) {
|
||||
if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER;
|
||||
const char* cursor = date_time;
|
||||
int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0;
|
||||
bool ok = parseDigits(cursor, 4, year) && *cursor++ == '-' && parseDigits(cursor, 2, month) && *cursor++ == '-' && parseDigits(cursor, 2, day);
|
||||
ok = ok && (*cursor == 'T' || *cursor == 't' || *cursor == ' ');
|
||||
if (ok) ++cursor;
|
||||
ok = ok && parseDigits(cursor, 2, hour) && *cursor++ == ':' && parseDigits(cursor, 2, minute) && *cursor++ == ':' && parseDigits(cursor, 2, second);
|
||||
std::uint32_t microsecond = 0;
|
||||
if (ok && *cursor == '.') {
|
||||
++cursor;
|
||||
} else if (*cursor == '+' || *cursor == '-') {
|
||||
const int sign = *cursor++ == '-' ? -1 : 1;
|
||||
int offsetHours = 0, offsetMinute = 0;
|
||||
if (!parseDigits(cursor, 2, offsetHours) || *cursor++ != ':' || !parseDigits(cursor, 2, offsetMinute)) return SCE_RTC_ERROR_BAD_PARSE;
|
||||
if (offsetHours > 23 || offsetMinute > 59) return SCE_RTC_ERROR_BAD_PARSE;
|
||||
offsetMinutes = sign * (offsetHours * 60 + offsetMinute);
|
||||
} else {
|
||||
std::uint32_t scale = 100000;
|
||||
ok = *cursor >= '0' && *cursor <= '9';
|
||||
for (; ok && *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast<std::uint32_t>(*cursor - '0') * scale;
|
||||
}
|
||||
std::int64_t offsetMinutes = 0;
|
||||
if (ok && *cursor != 0) ok = parseZoneSuffix(cursor, offsetMinutes);
|
||||
if (ok && *cursor != 0) ok = false;
|
||||
if (!ok) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return SCE_RTC_ERROR_BAD_PARSE;
|
||||
}
|
||||
if (*cursor != 0) return SCE_RTC_ERROR_BAD_PARSE;
|
||||
const RtcDateTime time{static_cast<std::uint16_t>(year), static_cast<std::uint16_t>(month), static_cast<std::uint16_t>(day),
|
||||
static_cast<std::uint16_t>(hour), static_cast<std::uint16_t>(minute), static_cast<std::uint16_t>(second), microsecond};
|
||||
if (const int result = validate(&time); result != 0) return result;
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@ if(BUILD_TESTING)
|
|||
configure_windows_unwind(guest_system_exit_tests)
|
||||
add_test(NAME guest_system_exit_parameters COMMAND guest_system_exit_tests)
|
||||
add_test(NAME guest_system_exit COMMAND guest_system_exit_tests --exit)
|
||||
add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit)
|
||||
add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate)
|
||||
add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main)
|
||||
set_tests_properties(guest_system_exit PROPERTIES
|
||||
PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed")
|
||||
set_tests_properties(guest_quick_exit PROPERTIES
|
||||
PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed")
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp)
|
||||
|
|
|
|||
|
|
@ -160,12 +160,6 @@ std::uint64_t APS5_VABI libcCyberUnknown14(void) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("qdGFBoLVNKI", libcCyberUnknown15);
|
||||
std::uint64_t APS5_VABI libcCyberUnknown15(void) {
|
||||
NotImplemented_nid_no_patch("qdGFBoLVNKI");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("vEaqE-7IZYc", libcCyberUnknown16);
|
||||
std::uint64_t APS5_VABI libcCyberUnknown16(void) {
|
||||
NotImplemented_nid_no_patch("vEaqE-7IZYc");
|
||||
|
|
|
|||
|
|
@ -162,4 +162,27 @@ int APS5_VABI atexit_nid_postfix(GuestExitCallback func) {
|
|||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::mutex quickExitMutex;
|
||||
std::vector<GuestExitCallback> quickExitCallbacks;
|
||||
}
|
||||
|
||||
int APS5_VABI at_quick_exit_nid_postfix(GuestExitCallback func) {
|
||||
if (func == nullptr)
|
||||
return 0;
|
||||
std::lock_guard lock(quickExitMutex);
|
||||
quickExitCallbacks.push_back(func);
|
||||
return 0;
|
||||
}
|
||||
|
||||
[[noreturn]] void APS5_VABI quick_exit_nid_postfix(int status) {
|
||||
std::vector<GuestExitCallback> callbacks;
|
||||
{
|
||||
std::lock_guard lock(quickExitMutex);
|
||||
callbacks = std::move(quickExitCallbacks);
|
||||
}
|
||||
for (auto it = callbacks.rbegin(); it != callbacks.rend(); ++it) (*it)();
|
||||
_Exit_nid_postfix(status);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
#include <mutex>
|
||||
#include "Sse4aEmulation.hpp"
|
||||
|
|
@ -28,6 +30,8 @@ void Report(const char* format, ...) {
|
|||
WriteFile(GetStdHandle(STD_ERROR_HANDLE), buffer, static_cast<DWORD>(length), &written, nullptr);
|
||||
}
|
||||
|
||||
void ReportAllThreads();
|
||||
|
||||
void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) {
|
||||
HMODULE module = nullptr;
|
||||
char path[MAX_PATH] = "?";
|
||||
|
|
@ -330,12 +334,99 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) {
|
|||
}
|
||||
}
|
||||
std::fflush(stderr);
|
||||
ReportAllThreads();
|
||||
std::fflush(stderr);
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
std::size_t CaptureFrames(const CONTEXT* context, std::uint64_t* frames, std::size_t capacity) {
|
||||
std::size_t count = 0;
|
||||
std::uint64_t frame = context->Rbp;
|
||||
while (count < capacity && frame != 0 && (frame & 7) == 0 && IsReadable(frame) && IsReadable(frame + 8)) {
|
||||
const auto returnAddress = reinterpret_cast<const std::uint64_t*>(frame)[1];
|
||||
if (!IsExecutable(returnAddress)) break;
|
||||
frames[count++] = returnAddress;
|
||||
const auto next = reinterpret_cast<const std::uint64_t*>(frame)[0];
|
||||
if (next <= frame) break;
|
||||
frame = next;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name, const std::uint64_t* frames, std::size_t frameCount) {
|
||||
char line[MAX_PATH + 64];
|
||||
Report(" thread %lu '%s': rip ", static_cast<unsigned long>(threadId), name ? name : "");
|
||||
DescribeAddress(context->Rip, line, sizeof(line));
|
||||
Report("%s rsp 0x%016llx rbp 0x%016llx\n", line, static_cast<unsigned long long>(context->Rsp), static_cast<unsigned long long>(context->Rbp));
|
||||
for (std::size_t depth = 0; depth < frameCount; ++depth) {
|
||||
DescribeAddress(frames[depth], line, sizeof(line));
|
||||
Report(" #%llu %s\n", static_cast<unsigned long long>(depth), line);
|
||||
}
|
||||
}
|
||||
|
||||
void ReportAllThreads() {
|
||||
const DWORD current = GetCurrentThreadId();
|
||||
const DWORD pid = GetCurrentProcessId();
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) return;
|
||||
THREADENTRY32 entry{};
|
||||
entry.dwSize = sizeof(entry);
|
||||
struct CapturedThread {
|
||||
DWORD id;
|
||||
char name[128];
|
||||
CONTEXT context;
|
||||
std::uint64_t frames[12];
|
||||
std::size_t frameCount;
|
||||
};
|
||||
static CapturedThread captured[128];
|
||||
std::size_t capturedCount = 0;
|
||||
std::size_t skippedCount = 0;
|
||||
if (Thread32First(snapshot, &entry)) {
|
||||
do {
|
||||
if (entry.th32OwnerProcessID != pid || entry.th32ThreadID == current) continue;
|
||||
if (capturedCount >= sizeof(captured) / sizeof(captured[0])) {
|
||||
++skippedCount;
|
||||
continue;
|
||||
}
|
||||
HANDLE thread = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, entry.th32ThreadID);
|
||||
if (!thread) continue;
|
||||
if (SuspendThread(thread) == static_cast<DWORD>(-1)) {
|
||||
CloseHandle(thread);
|
||||
continue;
|
||||
}
|
||||
CONTEXT context{};
|
||||
context.ContextFlags = CONTEXT_FULL;
|
||||
const BOOL haveContext = GetThreadContext(thread, &context);
|
||||
std::uint64_t frames[12] = {};
|
||||
const std::size_t frameCount = haveContext ? CaptureFrames(&context, frames, sizeof(frames) / sizeof(frames[0])) : 0;
|
||||
ResumeThread(thread);
|
||||
char name[128] = "";
|
||||
PWSTR description = nullptr;
|
||||
if (SUCCEEDED(GetThreadDescription(thread, &description)) && description) {
|
||||
WideCharToMultiByte(CP_UTF8, 0, description, -1, name, sizeof(name), nullptr, nullptr);
|
||||
LocalFree(description);
|
||||
}
|
||||
CloseHandle(thread);
|
||||
if (!haveContext) continue;
|
||||
captured[capturedCount].id = entry.th32ThreadID;
|
||||
std::memcpy(captured[capturedCount].name, name, sizeof(captured[capturedCount].name));
|
||||
captured[capturedCount].context = context;
|
||||
std::memcpy(captured[capturedCount].frames, frames, sizeof(captured[capturedCount].frames));
|
||||
captured[capturedCount].frameCount = frameCount;
|
||||
++capturedCount;
|
||||
} while (Thread32Next(snapshot, &entry));
|
||||
}
|
||||
CloseHandle(snapshot);
|
||||
Report(" all threads:\n");
|
||||
for (std::size_t i = 0; i < capturedCount; ++i) {
|
||||
ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name, captured[i].frames, captured[i].frameCount);
|
||||
}
|
||||
if (skippedCount != 0) Report(" %llu further thread(s) left out\n", static_cast<unsigned long long>(skippedCount));
|
||||
}
|
||||
|
||||
// abort() and the UCRT's invalid-parameter path end the process with a silent fast fail
|
||||
// (0xc0000409); both are reported with the calling thread's stack first.
|
||||
void ReportBacktrace(const char* what) {
|
||||
void ReportBacktrace(const char* what, bool reportThreads) {
|
||||
void* frames[48];
|
||||
const auto count = RtlCaptureStackBackTrace(0, 48, frames, nullptr);
|
||||
Report("FATAL: %s on thread %lu\n", what, static_cast<unsigned long>(GetCurrentThreadId()));
|
||||
|
|
@ -344,22 +435,23 @@ void ReportBacktrace(const char* what) {
|
|||
DescribeAddress(reinterpret_cast<std::uint64_t>(frames[i]), line, sizeof(line));
|
||||
Report(" #%u %s\n", static_cast<unsigned>(i), line);
|
||||
}
|
||||
if (reportThreads) ReportAllThreads();
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
void AbortSignalHandler(int) {
|
||||
ReportBacktrace("abort() (SIGABRT)");
|
||||
ReportBacktrace("abort() (SIGABRT)", true);
|
||||
}
|
||||
|
||||
void InvalidParameterHandler(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) {
|
||||
ReportBacktrace("UCRT invalid parameter");
|
||||
ReportBacktrace("UCRT invalid parameter", true);
|
||||
}
|
||||
|
||||
// Static destruction runs on the thread that ends the process (ExitProcess -> DLL_PROCESS_DETACH),
|
||||
// so its stack names the exit's caller when no libc exit/abort path was taken.
|
||||
struct ExitReporter {
|
||||
~ExitReporter() {
|
||||
if (std::getenv("APS5_TRACE_EXIT") != nullptr) ReportBacktrace("process exit (static destruction)");
|
||||
if (std::getenv("APS5_TRACE_EXIT") != nullptr) ReportBacktrace("process exit (static destruction)", false);
|
||||
}
|
||||
} g_exitReporter;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceErrorDialogInitialize(void);
|
||||
int APS5_VABI sceErrorDialogOpen(const void* param);
|
||||
int APS5_VABI sceErrorDialogUpdateStatus(void);
|
||||
int APS5_VABI sceErrorDialogGetStatus(void);
|
||||
int APS5_VABI sceErrorDialogClose(void);
|
||||
int APS5_VABI sceErrorDialogTerminate(void);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
constexpr int kStatusRunning = 2;
|
||||
constexpr int kStatusFinished = 3;
|
||||
constexpr int kErrNotInitialized = static_cast<int>(0x80ED0001);
|
||||
constexpr int kErrAlreadyInitialized = static_cast<int>(0x80ED0002);
|
||||
constexpr int kErrParam = static_cast<int>(0x80ED0003);
|
||||
constexpr int kErrInvalidState = static_cast<int>(0x80ED0005);
|
||||
|
||||
struct ErrorDialogParam {
|
||||
std::int32_t size;
|
||||
std::int32_t error_code;
|
||||
std::int32_t user_id;
|
||||
std::int32_t reserved;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
ErrorDialogParam param{16, static_cast<std::int32_t>(0x80020001), 0, 0};
|
||||
|
||||
Require(sceErrorDialogTerminate() == kErrNotInitialized);
|
||||
Require(sceErrorDialogOpen(¶m) == kErrInvalidState);
|
||||
|
||||
Require(sceErrorDialogInitialize() == 0);
|
||||
Require(sceErrorDialogInitialize() == kErrAlreadyInitialized);
|
||||
|
||||
Require(sceErrorDialogClose() == kErrInvalidState);
|
||||
|
||||
Require(sceErrorDialogOpen(nullptr) == kErrParam);
|
||||
ErrorDialogParam bad{8, 0, 0, 0};
|
||||
Require(sceErrorDialogOpen(&bad) == kErrParam);
|
||||
|
||||
Require(sceErrorDialogOpen(¶m) == 0);
|
||||
Require(sceErrorDialogGetStatus() == kStatusRunning);
|
||||
Require(sceErrorDialogOpen(¶m) == kErrInvalidState);
|
||||
|
||||
Require(sceErrorDialogUpdateStatus() == kStatusFinished);
|
||||
Require(sceErrorDialogGetStatus() == kStatusFinished);
|
||||
|
||||
Require(sceErrorDialogOpen(¶m) == 0);
|
||||
Require(sceErrorDialogGetStatus() == kStatusRunning);
|
||||
Require(sceErrorDialogClose() == 0);
|
||||
Require(sceErrorDialogGetStatus() == kStatusFinished);
|
||||
|
||||
Require(sceErrorDialogTerminate() == 0);
|
||||
Require(sceErrorDialogTerminate() == kErrNotInitialized);
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool);
|
|||
int APS5_VABI scePadInit_nid_postfix(void);
|
||||
int APS5_VABI scePadSetTiltCorrectionState(int, bool);
|
||||
int APS5_VABI scePadResetOrientation(int);
|
||||
int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool);
|
||||
}
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
|
@ -60,4 +61,6 @@ int main() {
|
|||
Require(scePadClose_nid_postfix(handle) == 0);
|
||||
Require(scePadGetHandle(user, 0, 0) == noHandle);
|
||||
Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0);
|
||||
Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0);
|
||||
Require(scePadSetAngularVelocityDeadbandState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceRtcCheckValid(const RtcDateTime*);
|
||||
|
|
@ -21,6 +22,7 @@ int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime*, std::uint64_t*);
|
|||
int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime*, std::uint64_t);
|
||||
int APS5_VABI sceRtcFormatRFC3339(char*, const RtcTick*, int);
|
||||
int APS5_VABI sceRtcParseRFC3339(RtcTick*, const char*);
|
||||
int APS5_VABI sceRtcParseDateTime(RtcTick*, const char*);
|
||||
int APS5_VABI sceRtcTickAddTicks(RtcTick*, const RtcTick*, std::int64_t);
|
||||
int APS5_VABI sceRtcTickAddSeconds(RtcTick*, const RtcTick*, std::int64_t);
|
||||
int APS5_VABI sceRtcTickAddDays(RtcTick*, const RtcTick*, std::int32_t);
|
||||
|
|
@ -123,6 +125,30 @@ int main() {
|
|||
Require(sceRtcParseRFC3339(&tick, "2024-02-29T12:34:56.789-23:59") == 0 && tick.tick == leapDayTick + 86340000000ull);
|
||||
Require(sceRtcParseRFC3339(nullptr, "1970-01-01T00:00:00Z") == invalidPointer);
|
||||
|
||||
Require(sceRtcParseDateTime(&tick, "2024-02-29T12:34:56.789") == 0 && tick.tick == leapDayTick);
|
||||
Require(sceRtcParseDateTime(&tick, "2024-02-29 12:34:56") == 0 && tick.tick == leapDayTick - 789000ull);
|
||||
Require(sceRtcParseDateTime(&tick, "2024-02-29T14:04:56.789+01:30") == 0 && tick.tick == leapDayTick);
|
||||
Require(sceRtcParseDateTime(&tick, "2024-02-29t12:34:56.789z") == 0 && tick.tick == leapDayTick);
|
||||
Require(sceRtcParseDateTime(&tick, "1970-01-01T00:00:00Z") == 0 && tick.tick == unixEpochTick);
|
||||
Require(sceRtcParseDateTime(&tick, "2023-02-29T00:00:00") == invalidDay);
|
||||
Require(sceRtcParseDateTime(nullptr, "1970-01-01T00:00:00Z") == invalidPointer);
|
||||
const char* unparseable[] = {
|
||||
"2024-02-29",
|
||||
"2024/02/29T12:34:56",
|
||||
"2024-02-29T12:34:56Zjunk",
|
||||
"2024-02-29T12:34:56+99:99",
|
||||
"2024-02-29T12:34:56.",
|
||||
};
|
||||
for (const char* text : unparseable) {
|
||||
bool thrown = false;
|
||||
try {
|
||||
sceRtcParseDateTime(&tick, text);
|
||||
} catch (const std::exception&) {
|
||||
thrown = true;
|
||||
}
|
||||
Require(thrown);
|
||||
}
|
||||
|
||||
RtcTick source{leapDayTick};
|
||||
RtcTick result{};
|
||||
Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*);
|
||||
extern "C" void APS5_VABI _Exit_nid_postfix(int);
|
||||
extern "C" void APS5_VABI catchReturnFromMain_nid_postfix(int);
|
||||
using GuestQuickExitCallback = void (APS5_VABI*)();
|
||||
extern "C" int APS5_VABI at_quick_exit_nid_postfix(GuestQuickExitCallback);
|
||||
extern "C" void APS5_VABI quick_exit_nid_postfix(int);
|
||||
namespace {
|
||||
bool cleaned = false;
|
||||
void Cleanup() { cleaned = true; }
|
||||
|
|
@ -15,6 +18,13 @@ void VerifyExit() {
|
|||
if (!cleaned) std::_Exit(1);
|
||||
std::puts("Guest shutdown and atexit completed");
|
||||
}
|
||||
int quickOrder = 0;
|
||||
void APS5_VABI QuickSecond() { quickOrder = 1; }
|
||||
void APS5_VABI QuickFirst() {
|
||||
if (quickOrder != 1) std::_Exit(1);
|
||||
std::puts("Guest quick_exit handlers completed");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
void Require(bool value) { if (!value) std::abort(); }
|
||||
void UnexpectedCleanup() { std::_Exit(3); }
|
||||
}
|
||||
|
|
@ -29,6 +39,12 @@ int main(int argc, char** argv) {
|
|||
catchReturnFromMain_nid_postfix(0);
|
||||
return 2;
|
||||
}
|
||||
if (argc > 1 && std::strcmp(argv[1], "--quick-exit") == 0) {
|
||||
Require(at_quick_exit_nid_postfix(QuickFirst) == 0);
|
||||
Require(at_quick_exit_nid_postfix(QuickSecond) == 0);
|
||||
quick_exit_nid_postfix(0);
|
||||
return 2;
|
||||
}
|
||||
if (argc > 1) {
|
||||
LibcRegisterShutdown_nid_postfix(Cleanup);
|
||||
Require(std::atexit(VerifyExit) == 0);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#include <codegen/x86/X64InstructionDecoder.hpp>
|
||||
#include <codegen/CodegenException.hpp>
|
||||
#include <io/BufferUtils.hpp>
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
|
@ -173,6 +175,8 @@ std::set<Domain::VirtualAddress> CodeInstructionCollector::Collect(const std::ve
|
|||
if (roots.empty()) throw Domain::RelinkerException("Code analysis: no code entry points");
|
||||
std::set<std::uint64_t> instructions;
|
||||
const Codegen::X64InstructionDecoder decoder;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> skipped;
|
||||
std::set<std::uint64_t> staticTargets;
|
||||
for (const auto& [begin, end] : functions) {
|
||||
const auto offset = fileOffset(begin, end - begin);
|
||||
for (auto address = begin; address < end;) {
|
||||
|
|
@ -180,16 +184,36 @@ std::set<Domain::VirtualAddress> CodeInstructionCollector::Collect(const std::ve
|
|||
try {
|
||||
info = decoder.DecodeInstruction(bytes.data() + offset + address - begin, end - address);
|
||||
} catch (const Codegen::CodegenException& error) {
|
||||
throw Domain::RelinkerException(std::string("Code analysis: ") + error.what(), address);
|
||||
const auto tailBytes = end - address;
|
||||
bool tailHasFsPrefix = false;
|
||||
for (auto probe = address; probe < end; ++probe) {
|
||||
if (bytes[offset + probe - begin] == 0x64) {
|
||||
tailHasFsPrefix = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tailBytes > 15 || tailHasFsPrefix)
|
||||
throw Domain::RelinkerException(std::string("Code analysis: ") + error.what(), address);
|
||||
skipped.emplace_back(address, end);
|
||||
break;
|
||||
}
|
||||
if (info.Length == 0 || info.Length > end - address)
|
||||
throw Domain::RelinkerException("Code analysis: instruction crosses function boundary", address);
|
||||
instructions.insert(address);
|
||||
if (info.HasBranchTarget && !info.HasRipRelativeDisp)
|
||||
addRoot(address + info.Length + static_cast<std::uint64_t>(info.BranchDisp));
|
||||
if (info.HasBranchTarget && !info.HasRipRelativeDisp) {
|
||||
const auto target = address + info.Length + static_cast<std::uint64_t>(info.BranchDisp);
|
||||
staticTargets.insert(target);
|
||||
addRoot(target);
|
||||
}
|
||||
address += info.Length;
|
||||
}
|
||||
}
|
||||
for (const auto target : staticTargets) {
|
||||
for (const auto& [skipBegin, skipEnd] : skipped) {
|
||||
if (skipBegin <= target && target < skipEnd)
|
||||
throw Domain::RelinkerException("Code analysis: branch into skipped range tail", target);
|
||||
}
|
||||
}
|
||||
std::size_t previousRoots = 0;
|
||||
do {
|
||||
previousRoots = roots.size();
|
||||
|
|
@ -211,7 +235,47 @@ std::set<Domain::VirtualAddress> CodeInstructionCollector::Collect(const std::ve
|
|||
} while (roots.size() != previousRoots);
|
||||
std::uint64_t previousEnd = 0;
|
||||
for (const auto address : instructions) {
|
||||
if (address < previousEnd) throw Domain::RelinkerException("Code analysis: overlapping instruction boundaries", address);
|
||||
if (address < previousEnd) {
|
||||
std::uint64_t spanEnd = previousEnd;
|
||||
try {
|
||||
for (const auto& header : headers) {
|
||||
if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue;
|
||||
const auto offset = address - header.MappedAddress;
|
||||
const auto length = decoder.DecodeInstruction(bytes.data() + header.Offset + offset, header.FileSize - offset).Length;
|
||||
if (length == 0 || length > header.FileSize - offset) break;
|
||||
spanEnd = std::max(spanEnd, address + length);
|
||||
break;
|
||||
}
|
||||
} catch (const Codegen::CodegenException&) {
|
||||
}
|
||||
bool spanHasFsPrefix = false;
|
||||
for (const auto& header : headers) {
|
||||
if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue;
|
||||
const auto base = header.Offset + address - header.MappedAddress;
|
||||
for (auto probe = base; probe < base + (spanEnd - address) && probe < bytes.size(); ++probe) {
|
||||
if (bytes[probe] == 0x64) {
|
||||
spanHasFsPrefix = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!spanHasFsPrefix) {
|
||||
const auto previous = instructions.lower_bound(address);
|
||||
if (previous != instructions.begin()) {
|
||||
const auto previousStart = *std::prev(previous);
|
||||
for (const auto& header : headers) {
|
||||
if (header.Type != 1 || (header.Flags & 1) == 0 || previousStart < header.MappedAddress || previousStart - header.MappedAddress >= header.FileSize) continue;
|
||||
const auto base = header.Offset + previousStart - header.MappedAddress;
|
||||
if (decoder.DecodeInstruction(bytes.data() + base, bytes.size() - base).SegmentPrefix == 0x64) spanHasFsPrefix = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spanHasFsPrefix) throw Domain::RelinkerException("Code analysis: overlapping instruction boundaries", address);
|
||||
previousEnd = spanEnd;
|
||||
continue;
|
||||
}
|
||||
for (const auto& header : headers) {
|
||||
if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue;
|
||||
const auto offset = address - header.MappedAddress;
|
||||
|
|
|
|||
|
|
@ -235,6 +235,14 @@ def main():
|
|||
overlapping = make_image("register", "unwind")
|
||||
overlapping[0x1200:0x1205] = b"\xe9" + struct.pack("<i", 0x1245 - 0x1205)
|
||||
convert("overlapping-entry", overlapping, "Code analysis: overlapping instruction boundaries")
|
||||
fs_overlap = make_image("register", "unwind")
|
||||
fs_overlap[0x1209:0x1211] = bytes.fromhex("48 8b 04 25 64 00 00 00")
|
||||
fs_overlap[0x1211:0x1216] = b"\xe8" + struct.pack("<i", 0x120D - 0x1216)
|
||||
convert("overlapping-fs-prefix", fs_overlap, "Code analysis: overlapping instruction boundaries")
|
||||
fs66_overlap = make_image("register", "unwind")
|
||||
fs66_overlap[0x1209:0x1216] = bytes.fromhex("66 66 66 66 64 48 8b 04 25 78 56 00 00")
|
||||
fs66_overlap[0x1216:0x121B] = b"\xe8" + struct.pack("<i", 0x1211 - 0x121B)
|
||||
convert("overlapping-fs-prefix-66", fs66_overlap, "Code analysis: overlapping instruction boundaries")
|
||||
external = make_image("register", "unwind")
|
||||
external[0x1300:0x1310] = external[0x1240:0x1250]
|
||||
external[0x1240:0x1250] = b"\xe8" + struct.pack("<i", 0x1300 - 0x1245) + b"\xc3" + b"\x90" * 10
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except:
|
||||
- [libSceSaveDataDialog.native](../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp)
|
||||
- [libSceCommonDialog](../../core/libs/prx/libSceCommonDialog/Export.cpp)
|
||||
- [libSceErrorDialog](../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged
|
||||
- [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified.
|
||||
- [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified.
|
||||
- [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls do nothing
|
||||
|
|
@ -24,6 +25,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
### Unknown function info
|
||||
|
||||
- [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown
|
||||
- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`); a string without a zone is taken as UTC, and anything outside this shape throws instead of returning a parse error, unverified against hardware
|
||||
- [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order
|
||||
- [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded
|
||||
- [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted
|
||||
|
|
@ -43,6 +45,8 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature
|
||||
- [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature
|
||||
- [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature
|
||||
- [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted
|
||||
- [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function
|
||||
- [AOWqIYsgVHs](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - unknown name, signature
|
||||
- [GQTObcITIXI](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - unknown name, signature
|
||||
- [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
PRX = ROOT / "core" / "libs" / "prx"
|
||||
DB_URL = ("https://raw.githubusercontent.com/zecoxao/sce_symbols"
|
||||
"/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv")
|
||||
DEFAULT_CACHE = Path(tempfile.gettempdir()) / "anyps5-nid-db" / "aerolib.csv"
|
||||
|
||||
CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-"
|
||||
SUFFIX = bytes([0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1,
|
||||
0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30])
|
||||
ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w*[Uu]nknown\w*)\)')
|
||||
DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?\{")
|
||||
NID_POSTFIX = "_nid_postfix"
|
||||
|
||||
|
||||
def compute_nid(symbol):
|
||||
digest = hashlib.sha1(symbol.encode() + SUFFIX).digest()
|
||||
rev = bytes(digest[7 - i] for i in range(8))
|
||||
out = []
|
||||
for i in range(0, 6, 3):
|
||||
triple = (rev[i] << 16) | (rev[i + 1] << 8) | rev[i + 2]
|
||||
out += [CHARSET[(triple >> 18) & 63], CHARSET[(triple >> 12) & 63],
|
||||
CHARSET[(triple >> 6) & 63], CHARSET[triple & 63]]
|
||||
tail = (rev[6] << 16) | (rev[7] << 8)
|
||||
out += [CHARSET[(tail >> 18) & 63], CHARSET[(tail >> 12) & 63],
|
||||
CHARSET[(tail >> 6) & 63]]
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def load_db(path):
|
||||
if not path.exists():
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
print("downloading NID database to %s ..." % path, file=sys.stderr)
|
||||
urllib.request.urlretrieve(DB_URL, str(path))
|
||||
db = {}
|
||||
with open(path, errors="replace") as handle:
|
||||
for line in handle:
|
||||
parts = line.strip().split()
|
||||
if len(parts) >= 2:
|
||||
db.setdefault(parts[0], parts[1])
|
||||
return db
|
||||
|
||||
|
||||
def collect_unknowns():
|
||||
found = {}
|
||||
for path in sorted(PRX.rglob("*.cpp")):
|
||||
try:
|
||||
text = path.read_text(errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
for match in ALIAS.finditer(text):
|
||||
found.setdefault(match.group(1), "%s:%s" % (
|
||||
path.relative_to(ROOT), match.group(2)))
|
||||
return found
|
||||
|
||||
|
||||
def collect_real_names():
|
||||
names = set()
|
||||
for path in sorted((ROOT / "core" / "libs").rglob("*.cpp")):
|
||||
try:
|
||||
text = path.read_text(errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
for match in DEFINITION.finditer(text):
|
||||
name = match.group(1)
|
||||
names.add(name)
|
||||
if name.endswith(NID_POSTFIX):
|
||||
names.add(name[:-len(NID_POSTFIX)])
|
||||
return names
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Suggest real names for Unknown NID stubs")
|
||||
parser.add_argument("--db", default=os.environ.get("ANYPS5_NID_DB"),
|
||||
help="NID database CSV (downloaded to a cache dir by default)")
|
||||
parser.add_argument("--nid", nargs="*", default=[],
|
||||
help="resolve only these NIDs instead of scanning the tree")
|
||||
parser.add_argument("--json", action="store_true",
|
||||
help="emit machine-readable JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
db_path = Path(args.db) if args.db else DEFAULT_CACHE
|
||||
db = load_db(db_path)
|
||||
|
||||
if args.nid:
|
||||
unknowns = {nid: "<cli>" for nid in args.nid}
|
||||
else:
|
||||
unknowns = collect_unknowns()
|
||||
real_names = collect_real_names()
|
||||
|
||||
rows = []
|
||||
failures = 0
|
||||
for nid in sorted(unknowns):
|
||||
suggestion = db.get(nid, "")
|
||||
status = "unknown"
|
||||
if suggestion:
|
||||
if compute_nid(suggestion) != nid:
|
||||
status = "MISMATCH"
|
||||
failures += 1
|
||||
elif suggestion in real_names:
|
||||
status = "already-implemented"
|
||||
else:
|
||||
status = "rename-ready"
|
||||
rows.append({"nid": nid, "stub": unknowns[nid],
|
||||
"suggestion": suggestion, "status": status})
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({"db_entries": len(db), "rows": rows}, indent=1))
|
||||
else:
|
||||
print("%-12s %-40s %-12s %s" % ("NID", "STUB", "STATUS", "SUGGESTION"))
|
||||
for row in rows:
|
||||
print("%-12s %-40s %-12s %s" % (
|
||||
row["nid"], row["stub"][:40], row["status"], row["suggestion"]))
|
||||
print("%d unknowns, %d suggestions (%d already implemented)" % (
|
||||
len(rows), sum(1 for r in rows if r["suggestion"]),
|
||||
sum(1 for r in rows if r["status"] == "already-implemented")))
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in New Issue