Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions core/libs/prx/libkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,15 @@ if(BUILD_TESTING)
set_tests_properties(guest_sigtimedwait PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_itimer_tests ${LIBS_INCLUDE_DIR}/tests/GuestItimer.cpp)
target_include_directories(guest_itimer_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_itimer_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_itimer_tests)
add_test(NAME guest_itimer COMMAND guest_itimer_tests)
if(WIN32)
set_tests_properties(guest_itimer PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_parent_process_tests ${LIBS_INCLUDE_DIR}/tests/GuestParentProcess.cpp)
target_include_directories(guest_parent_process_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_parent_process_tests PRIVATE libkernel libc)
Expand Down
92 changes: 83 additions & 9 deletions core/libs/prx/libkernel/System/src/Signals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ struct AlarmState {
std::mutex lock;
std::condition_variable changed;
std::optional<std::chrono::steady_clock::time_point> deadline;
std::chrono::microseconds interval{0};
bool threadStarted = false;
};

Expand All @@ -98,19 +99,28 @@ AlarmState& Alarms() {
return *state;
}

void ExpireAlarm() {
std::string DescribeInterval(std::chrono::microseconds interval) {
char text[64];
std::snprintf(text, sizeof(text), "%lld.%06lld s", static_cast<long long>(interval.count() / 1000000),
static_cast<long long>(interval.count() % 1000000));
return text;
}

void ExpireAlarm(std::chrono::microseconds interval) {
std::uintptr_t handler = 0;
{
std::lock_guard lock(registration);
handler = dispositions[GuestSigalrm].handler;
}
if (handler == 1) return;
if ((blockedMask.load() & (1u << (GuestSigalrm - 1))) != 0)
throw std::runtime_error("ITIMER_REAL (it_interval " + DescribeInterval(interval) + ") expired while SIGALRM is blocked: pending signals are not implemented");
if (handler == 0) {
std::fprintf(stderr, "SIGALRM: the alarm expired with the default disposition; the process terminates\n");
std::fflush(stderr);
std::_Exit(128 + GuestSigalrm);
}
throw std::runtime_error("alarm: delivering SIGALRM to a guest handler is not implemented");
throw std::runtime_error("ITIMER_REAL (it_interval " + DescribeInterval(interval) + ") expired: delivering SIGALRM to a guest handler is not implemented");
}

void RunAlarms() {
Expand All @@ -123,9 +133,15 @@ void RunAlarms() {
}
const auto deadline = *alarms.deadline;
if (alarms.changed.wait_until(lock, deadline) != std::cv_status::timeout || alarms.deadline != deadline) continue;
alarms.deadline.reset();
const auto interval = alarms.interval;
if (interval.count() == 0) alarms.deadline.reset();
else {
const auto now = std::chrono::steady_clock::now();
const auto periods = now < deadline ? 1 : (now - deadline) / interval + 1;
alarms.deadline = deadline + periods * interval;
}
lock.unlock();
ExpireAlarm();
ExpireAlarm(interval);
lock.lock();
}
}
Expand All @@ -135,11 +151,10 @@ extern "C" unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds)
auto& alarms = Alarms();
std::lock_guard lock(alarms.lock);
const auto now = std::chrono::steady_clock::now();
unsigned int remaining = 0;
if (alarms.deadline && *alarms.deadline > now) {
const auto micros = std::chrono::duration_cast<std::chrono::microseconds>(*alarms.deadline - now).count();
remaining = static_cast<unsigned int>((micros + 999999) / 1000000);
}
const auto micros = alarms.deadline && *alarms.deadline > now
? std::chrono::duration_cast<std::chrono::microseconds>(*alarms.deadline - now).count() : 0;
const auto remaining = static_cast<unsigned int>((micros + 999999) / 1000000);
alarms.interval = std::chrono::microseconds(0);
if (seconds == 0) alarms.deadline.reset();
else alarms.deadline = now + std::chrono::seconds(seconds);
if (!alarms.threadStarted) {
Expand Down Expand Up @@ -181,6 +196,65 @@ extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output
return 0;
}

namespace {
constexpr int ItimerReal = 0;
constexpr int ItimerProf = 2;
constexpr std::int64_t MaxItimerSeconds = 0x7fffffff / 2;
constexpr std::int64_t MinExactIntervalMicros = 1000;

bool ValidItimer(const KernelTimeval& time) {
return time.tv_sec >= 0 && time.tv_usec >= 0 && time.tv_usec < 1000000 && time.tv_sec <= MaxItimerSeconds;
}
bool ItimerSet(const KernelTimeval& time) { return time.tv_sec != 0 || time.tv_usec != 0; }
std::chrono::microseconds ItimerDuration(const KernelTimeval& time) {
return std::chrono::seconds(time.tv_sec) + std::chrono::microseconds(time.tv_usec);
}
KernelTimeval ItimerTimeval(std::chrono::microseconds duration) {
return {duration.count() / 1000000, duration.count() % 1000000};
}
}

extern "C" int APS5_VABI setitimer_nid_postfix(int which, const GuestItimerval* value, GuestItimerval* previous) {
if (which < ItimerReal || which > ItimerProf) { *__error_nid_postfix() = 22; return -1; }
if (value == nullptr && previous == nullptr) { *__error_nid_postfix() = 14; return -1; }
GuestItimerval requested{};
if (value != nullptr) {
requested = *value;
if (!ValidItimer(requested.value)) { *__error_nid_postfix() = 22; return -1; }
if (!ItimerSet(requested.value)) requested.interval = {};
else if (!ValidItimer(requested.interval)) { *__error_nid_postfix() = 22; return -1; }
}
if (which != ItimerReal) {
if (ItimerSet(requested.value))
throw std::runtime_error(std::string("setitimer: ") + (which == ItimerProf ? "ITIMER_PROF" : "ITIMER_VIRTUAL") + " needs process CPU time accounting, which is not implemented");
if (previous != nullptr) *previous = {};
return 0;
}
if (requested.interval.tv_sec == 0 && requested.interval.tv_usec != 0 && requested.interval.tv_usec < MinExactIntervalMicros)
throw std::runtime_error("setitimer: it_interval " + DescribeInterval(ItimerDuration(requested.interval)) + " is below 1 ms, where the kernel rounds it up to a clock tick that is not known");
GuestItimerval old{};
auto& alarms = Alarms();
{
std::lock_guard lock(alarms.lock);
const auto now = std::chrono::steady_clock::now();
old.interval = ItimerTimeval(alarms.interval);
if (alarms.deadline && *alarms.deadline > now)
old.value = ItimerTimeval(std::chrono::duration_cast<std::chrono::microseconds>(*alarms.deadline - now));
if (value != nullptr) {
alarms.interval = ItimerDuration(requested.interval);
if (ItimerSet(requested.value)) alarms.deadline = now + ItimerDuration(requested.value);
else alarms.deadline.reset();
if (!alarms.threadStarted) {
std::thread(RunAlarms).detach();
alarms.threadStarted = true;
}
alarms.changed.notify_all();
}
}
if (previous != nullptr) *previous = old;
return 0;
}

struct GuestStack {
void* sp;
std::size_t size;
Expand Down
159 changes: 159 additions & 0 deletions core/libs/tests/GuestItimer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <chrono>
#include <climits>
#include <csignal>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <thread>
#include <typeinfo>

struct GuestItimerval {
KernelTimeval interval;
KernelTimeval value;
};
static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16);

using Handler = void (APS5_VABI *)(int);
using TerminateHandler = void (*)();
extern "C" {
int APS5_VABI setitimer_nid_postfix(int, const GuestItimerval*, GuestItimerval*);
unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int);
Handler APS5_VABI signal_nid_postfix(int, Handler);
int* APS5_VABI __error_nid_postfix();
void* APS5_VABI __cxa_current_primary_exception_nid_postfix();
std::type_info* APS5_VABI __cxa_current_exception_type_nid_postfix();
TerminateHandler APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(TerminateHandler);
}

static void Require(bool value, const char* message) {
if (!value) {
std::fprintf(stderr, "%s\n", message);
std::abort();
}
}

static std::int64_t Micros(const KernelTimeval& time) {
Require(time.tv_sec >= 0 && time.tv_usec >= 0 && time.tv_usec < 1000000, "timeval is not normalized");
return time.tv_sec * 1000000 + time.tv_usec;
}

static GuestItimerval Read(int which) {
GuestItimerval current{{11, 22}, {33, 44}};
*__error_nid_postfix() = 12345;
Require(setitimer_nid_postfix(which, nullptr, &current) == 0, "reading the timer failed");
Require(*__error_nid_postfix() == 12345, "reading the timer changed errno");
return current;
}

static void RequireFailure(int which, const GuestItimerval* value, int error, const char* message) {
GuestItimerval previous{{11, 22}, {33, 44}};
const auto saved = previous;
*__error_nid_postfix() = 0;
Require(setitimer_nid_postfix(which, value, &previous) == -1 && *__error_nid_postfix() == error, message);
Require(std::memcmp(&previous, &saved, sizeof(previous)) == 0, "a failed setitimer wrote the previous value");
}

volatile std::sig_atomic_t delivered = 0;
static void APS5_VABI OnAlarm(int signal) {
delivered = signal;
}

static void OnTerminate() {
const auto* type = __cxa_current_exception_type_nid_postfix();
const auto* error = static_cast<const std::runtime_error*>(__cxa_current_primary_exception_nid_postfix());
if (type != nullptr && error != nullptr && std::strcmp(type->name(), typeid(std::runtime_error).name()) == 0
&& std::strstr(error->what(), "it_interval 0.250000 s") != nullptr && delivered == 0) {
std::printf("expiry threw: %s\n", error->what());
std::fflush(stdout);
std::_Exit(0);
}
std::fprintf(stderr, "unexpected termination at expiry\n");
std::_Exit(1);
}

int main() {
const auto ignore = reinterpret_cast<Handler>(std::uintptr_t{1});
Require(signal_nid_postfix(14, ignore) == nullptr, "SIGALRM was not initially defaulted");
const GuestItimerval valid{{0, 0}, {30, 0}};
for (int invalid : {-1, 3, INT_MIN, INT_MAX}) {
RequireFailure(invalid, &valid, 22, "an unknown timer did not report EINVAL");
RequireFailure(invalid, nullptr, 22, "an unknown timer read did not report EINVAL");
*__error_nid_postfix() = 0;
Require(setitimer_nid_postfix(invalid, nullptr, nullptr) == -1 && *__error_nid_postfix() == 22, "EINVAL is not checked before EFAULT");
}
for (int which : {0, 1, 2}) {
*__error_nid_postfix() = 0;
Require(setitimer_nid_postfix(which, nullptr, nullptr) == -1 && *__error_nid_postfix() == 14, "a read into null did not report EFAULT");
for (const GuestItimerval bad : {GuestItimerval{{0, 0}, {-1, 0}}, GuestItimerval{{0, 0}, {0, -1}},
GuestItimerval{{0, 0}, {0, 1000000}}, GuestItimerval{{0, 0}, {INT_MAX / 2 + 1, 0}},
GuestItimerval{{-1, 0}, {1, 0}}, GuestItimerval{{0, 1000000}, {1, 0}}, GuestItimerval{{INT_MAX / 2 + 1, 0}, {1, 0}}})
RequireFailure(which, &bad, 22, "an invalid timeval did not report EINVAL");
const GuestItimerval disarm{{-5, 7}, {0, 0}};
GuestItimerval previous{{11, 22}, {33, 44}};
Require(setitimer_nid_postfix(which, &disarm, &previous) == 0, "disarming with an invalid interval failed");
Require(Micros(previous.interval) == 0 && Micros(previous.value) == 0, "an idle timer reported a value");
Require(Micros(Read(which).interval) == 0, "disarming kept the interval");
}
for (int cpu : {1, 2}) {
GuestItimerval previous{{11, 22}, {33, 44}};
const auto saved = previous;
bool threw = false;
try {
setitimer_nid_postfix(cpu, &valid, &previous);
} catch (const std::runtime_error&) {
threw = true;
}
Require(threw && std::memcmp(&previous, &saved, sizeof(previous)) == 0, "a CPU timer was armed");
Require(Micros(Read(cpu).value) == 0, "a CPU timer kept a value");
}
bool threw = false;
const GuestItimerval tiny{{0, 999}, {1, 0}};
try {
setitimer_nid_postfix(0, &tiny, nullptr);
} catch (const std::runtime_error&) {
threw = true;
}
Require(threw && Micros(Read(0).value) == 0, "an interval below the clock tick was armed");

GuestItimerval previous{};
Require(setitimer_nid_postfix(0, &valid, &previous) == 0 && Micros(previous.value) == 0, "arming ITIMER_REAL failed");
const auto first = Micros(Read(0).value);
Require(first > 29000000 && first <= 30000000, "the armed value is not visible");
std::this_thread::sleep_for(std::chrono::milliseconds(25));
const auto second = Micros(Read(0).value);
Require(second > 0 && second < first, "the remaining time does not count down");
const GuestItimerval repeating{{2, 500000}, {20, 0}};
Require(setitimer_nid_postfix(0, &repeating, &previous) == 0, "re-arming ITIMER_REAL failed");
Require(Micros(previous.interval) == 0 && Micros(previous.value) > 0 && Micros(previous.value) <= second, "re-arming did not return the previous value");
const GuestItimerval zero{{0, 0}, {0, 0}};
Require(setitimer_nid_postfix(0, &zero, &previous) == 0, "disarming ITIMER_REAL failed");
Require(Micros(previous.interval) == 2500000 && Micros(previous.value) > 19000000 && Micros(previous.value) <= 20000000,
"disarming did not return the previous value");
const auto idle = Read(0);
Require(Micros(idle.interval) == 0 && Micros(idle.value) == 0, "the disarmed timer is still set");

const GuestItimerval periodic{{0, 300000}, {0, 100000}};
Require(setitimer_nid_postfix(0, &periodic, nullptr) == 0, "arming a periodic timer failed");
std::this_thread::sleep_for(std::chrono::milliseconds(250));
const auto rearmed = Read(0);
Require(Micros(rearmed.interval) == 300000 && Micros(rearmed.value) > 0 && Micros(rearmed.value) <= 300000,
"the periodic timer was not re-armed by its interval");
Require(GuestAlarm_nid_no_patch(5) == 1, "alarm did not report the periodic timer");
const auto alarmed = Read(0);
Require(Micros(alarmed.interval) == 0 && Micros(alarmed.value) > 4000000 && Micros(alarmed.value) <= 5000000, "alarm did not replace the timer");
Require(setitimer_nid_postfix(0, &zero, &previous) == 0 && Micros(previous.value) > 0, "disarming the alarm failed");
Require(GuestAlarm_nid_no_patch(0) == 0, "setitimer did not cancel the alarm");

Require(signal_nid_postfix(14, OnAlarm) == ignore, "SIGALRM handler was not installed");
_ZSt13set_terminatePFvvE_nid_postfix(OnTerminate);
const GuestItimerval handled{{0, 250000}, {0, 50000}};
Require(setitimer_nid_postfix(0, &handled, nullptr) == 0, "arming the handled timer failed");
std::this_thread::sleep_for(std::chrono::seconds(2));
std::fprintf(stderr, "the expiry with a handler neither delivered nor threw\n");
return 1;
}
1 change: 0 additions & 1 deletion docs/dev/debt/libSceAgc.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,4 @@
### Functional

- [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents
- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed.
- [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well
Loading
Loading