mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-30 18:49:13 +02:00
Merge pull request #22002 from hrydgard/ai-autotest-workflow
AI pspautotests workflow description, and a few fixes that came out of it. Plus some manual sceReg implementation
This commit is contained in:
@@ -45,6 +45,10 @@ when making cross platform changes.
|
||||
New unit tests are added by listing them in availableTests in unittest.cpp. If they are large, put them in
|
||||
separate files in the unittest subdirectory. Remember to update both CMakeLists.txt and the visual studio project.
|
||||
|
||||
pspautotests are a large set of tests of the PSP OS's API surface, and thus tests our HLE implementation.
|
||||
|
||||
See docs/pspautotests.md for a workflow for running pspautotests and improving PPSSPP with the results.
|
||||
|
||||
## Adding HLE modules
|
||||
|
||||
HLE module implementations live in `Core/HLE/sce<ModuleName>.cpp` / `.h` (e.g. `sceOpenPSID.cpp`, `scePauth.cpp` are good
|
||||
|
||||
@@ -560,12 +560,69 @@ int sceKernelReferMbxStatus(SceUID id, u32 infoAddr) {
|
||||
if (!info.IsValid())
|
||||
return hleLogError(Log::sceKernel, -1, "invalid pointer");
|
||||
|
||||
u32 packet = m->nmb.packetListHead;
|
||||
// The PSP's ReferMbxStatus doesn't just read packetListHead — it traverses
|
||||
// the linked list and *updates* firstMessage to handle test programs that
|
||||
// corrupt the kernel-managed ->next pointers in PSP memory. This behavior
|
||||
// was confirmed from pspautotests expected output.
|
||||
//
|
||||
// The mailbox uses a circular linked list of NativeMbxPacket nodes:
|
||||
// head -> msg1 -> msg2 -> ... -> msgN -> head
|
||||
// A single message points to itself (head -> msg -> msg).
|
||||
// numMessages tracks the intended count separately from the chain.
|
||||
//
|
||||
// Three corruption patterns are handled:
|
||||
//
|
||||
// 1. NULL in the chain (msgK->next = 0):
|
||||
// The list is broken. The PSP sets firstMessage = NULL.
|
||||
// count stays unchanged (kernel owns it, user can't modify it).
|
||||
//
|
||||
// 2. Self-pointer with count mismatch (msgK->next = msgK, N > 1):
|
||||
// The test set a message's next to itself. The PSP detects this and
|
||||
// sets firstMessage = that message. Subsequent ReceiveMessage calls
|
||||
// then find a self-pointer where count > 1 and return PSP_MBX_ERROR_DUPLICATE_MSG.
|
||||
//
|
||||
// 3. Extra nodes beyond count (count=N but chain has M > N nodes):
|
||||
// The count-bound walk doesn't return to the original head. The PSP
|
||||
// keeps walking until it finds the node whose ->next == original_head
|
||||
// (the wrap point), then sets firstMessage = that node.
|
||||
// This happens when test code manually splices extra messages into the
|
||||
// circular chain after a normal sceKernelSendMbx (which only tracks
|
||||
// count of kernel-sent messages but writes ->next into PSP memory).
|
||||
u32 head = m->nmb.packetListHead;
|
||||
u32 packet = head;
|
||||
for (int i = 0, n = m->nmb.numMessages; i < n; ++i) {
|
||||
if (packet == 0 || !Memory::IsValidAddress(packet)) {
|
||||
return hleLogError(Log::sceKernel, -1, "invalid packet list head");
|
||||
m->nmb.packetListHead = 0;
|
||||
packet = 0;
|
||||
break;
|
||||
}
|
||||
u32 next = Memory::ReadUnchecked_U32(packet);
|
||||
if (next == 0) {
|
||||
m->nmb.packetListHead = 0;
|
||||
packet = 0;
|
||||
break;
|
||||
}
|
||||
if (next == packet) {
|
||||
if (n > 1)
|
||||
m->nmb.packetListHead = packet;
|
||||
break;
|
||||
}
|
||||
packet = next;
|
||||
}
|
||||
if (packet != 0 && packet != head) {
|
||||
// At most numMessages more steps to complete the circle back to head.
|
||||
for (int i = 0, n = m->nmb.numMessages; i < n; ++i) {
|
||||
if (packet == 0 || !Memory::IsValidAddress(packet))
|
||||
break;
|
||||
u32 next = Memory::ReadUnchecked_U32(packet);
|
||||
if (next == head) {
|
||||
m->nmb.packetListHead = packet;
|
||||
break;
|
||||
}
|
||||
if (next == 0 || next == packet)
|
||||
break;
|
||||
packet = next;
|
||||
}
|
||||
packet = Memory::ReadUnchecked_U32(packet);
|
||||
}
|
||||
|
||||
HLEKernel::CleanupWaitingThreads(WAITTYPE_MBX, id, m->waitingThreads);
|
||||
|
||||
+48
-6
@@ -944,10 +944,10 @@ static const KeyValue tree_REGISTRY[] = {
|
||||
|
||||
// There might be more categories.
|
||||
const KeyValue ROOT[] = {
|
||||
{ "REGISTRY", ValueType::DIR, "", ARRAY_SIZE(tree_REGISTRY), tree_REGISTRY },
|
||||
{ "CONFIG", ValueType::DIR, "", ARRAY_SIZE(tree_CONFIG), tree_CONFIG },
|
||||
{ "DATA", ValueType::DIR, "", ARRAY_SIZE(tree_DATA), tree_DATA },
|
||||
{ "SYSPROFILE", ValueType::DIR, "", ARRAY_SIZE(tree_SYSPROFILE), tree_SYSPROFILE },
|
||||
{ "CONFIG", ValueType::DIR, "", ARRAY_SIZE(tree_CONFIG), tree_CONFIG },
|
||||
{ "REGISTRY", ValueType::DIR, "", ARRAY_SIZE(tree_REGISTRY), tree_REGISTRY },
|
||||
};
|
||||
|
||||
// Updater checks for CONFIG/SYSTEM/XMB.
|
||||
@@ -1308,7 +1308,8 @@ int sceRegGetKeyValueByName(int catHandle, const char *name, u32 bufAddr, u32 si
|
||||
Memory::MemcpyUnchecked(bufAddr, keyval.strValue, std::min(size, (u32)keyval.intValue));
|
||||
return hleLogInfo(Log::sceReg, 0, "value: '%s'", keyval.strValue);
|
||||
case ValueType::INT:
|
||||
Memory::WriteUnchecked_U32(keyval.intValue, bufAddr);
|
||||
if (size >= sizeof(u32))
|
||||
Memory::WriteUnchecked_U32(keyval.intValue, bufAddr);
|
||||
return hleLogInfo(Log::sceReg, 0, "value: %d (0x%08x)", keyval.intValue, keyval.intValue);
|
||||
case ValueType::DIR:
|
||||
case ValueType::FAIL:
|
||||
@@ -1332,6 +1333,48 @@ int sceRegRemoveKey(int catHandle, int key) {
|
||||
return hleLogError(Log::sceReg, 0);
|
||||
}
|
||||
|
||||
int sceRegGetCategoryNumAtRoot(int regHandle, u32 numCategoriesPtr) {
|
||||
if (regHandle != 0) {
|
||||
return hleLogError(Log::sceReg, 0, "Not found");
|
||||
}
|
||||
|
||||
constexpr int numCategories = ARRAY_SIZE(ROOT);
|
||||
static_assert(numCategories == 4);
|
||||
|
||||
if (!Memory::IsValid4AlignedAddress(numCategoriesPtr)) {
|
||||
return hleLogError(Log::sceReg, -1, "Invalid pointer");
|
||||
}
|
||||
|
||||
Memory::WriteUnchecked_U32(numCategories, numCategoriesPtr);
|
||||
return hleLogInfo(Log::sceReg, 0);
|
||||
}
|
||||
|
||||
int sceRegGetCategoryListAtRoot(int regHandle, u32 bufPtr, int numCategories) {
|
||||
if (regHandle != 0) {
|
||||
return hleLogError(Log::sceReg, 0, "Not found");
|
||||
}
|
||||
|
||||
if (numCategories > ARRAY_SIZE(ROOT)) {
|
||||
WARN_LOG(Log::sceReg, "numCategories too large");
|
||||
numCategories = ARRAY_SIZE(ROOT);
|
||||
}
|
||||
|
||||
if (!Memory::IsValidRange(bufPtr, numCategories * 27)) {
|
||||
return hleLogError(Log::sceReg, -1, "bad output addr");
|
||||
}
|
||||
|
||||
for (int i = 0; i < numCategories; i++) {
|
||||
const KeyValue &kv = ROOT[i];
|
||||
_dbg_assert_msg_(kv.type == ValueType::DIR, "Unexpected non-dir in ROOT");
|
||||
char *dest = (char *)Memory::GetPointerWrite(bufPtr + i * 27);
|
||||
if (dest) {
|
||||
strncpy(dest, kv.name.c_str(), 27);
|
||||
}
|
||||
}
|
||||
|
||||
return hleLogInfo(Log::sceReg, 0);
|
||||
}
|
||||
|
||||
const HLEFunction sceReg[] = {
|
||||
{ 0x92E41280, &WrapI_UIU<sceRegOpenRegistry>, "sceRegOpenRegistry", 'i', "xix" },
|
||||
{ 0xFA8A5739, &WrapI_I<sceRegCloseRegistry>, "sceRegCloseRegistry", 'i', "i" },
|
||||
@@ -1351,9 +1394,8 @@ const HLEFunction sceReg[] = {
|
||||
{ 0x4CA16893, &WrapI_IC<sceRegRemoveCategory>, "sceRegRemoveCategory", 'i', "i" },
|
||||
{ 0x3615BC87, &WrapI_II<sceRegRemoveKey>, "sceRegRemoveKey", 'i', "ii" },
|
||||
{ 0x9B25EDF1, nullptr, "sceRegExit", 'i', "i" },
|
||||
// TODO: Add test for these.
|
||||
{ 0xBE8C1263, nullptr, "sceRegGetCategoryNumAtRoot", 'i', "ii" },
|
||||
{ 0x835ECE6F, nullptr, "sceRegGetCategoryListAtRoot", 'i', "ipi" },
|
||||
{ 0xBE8C1263, &WrapI_IU<sceRegGetCategoryNumAtRoot>, "sceRegGetCategoryNumAtRoot", 'i', "ii" },
|
||||
{ 0x835ECE6F, &WrapI_IUI<sceRegGetCategoryListAtRoot>, "sceRegGetCategoryListAtRoot", 'i', "ipi" },
|
||||
};
|
||||
|
||||
void Register_sceReg() {
|
||||
|
||||
+157
-2
@@ -796,10 +796,165 @@ static int sceRtcTickAddYears(u32 destTickPtr, u32 srcTickPtr, int numYears)
|
||||
return hleNoLog(0);
|
||||
}
|
||||
|
||||
struct RtcParseResult {
|
||||
ScePspDateTime date;
|
||||
int tzOffsetMinutes;
|
||||
bool ok;
|
||||
};
|
||||
|
||||
static const char *rtcParseMonthName(const char *p, int &month) {
|
||||
static const char *names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (p[0] == names[i][0] && p[1] == names[i][1] && p[2] == names[i][2]) {
|
||||
month = i + 1;
|
||||
return p + 3;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool rtcParseDigits(const char *&p, int n, int &value) {
|
||||
value = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (p[i] < '0' || p[i] > '9') return false;
|
||||
value = value * 10 + (p[i] - '0');
|
||||
}
|
||||
p += n;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool rtcParseRFC3339(const char *s, RtcParseResult &r) {
|
||||
r.ok = false;
|
||||
memset(&r.date, 0, sizeof(r.date));
|
||||
|
||||
int year, month, day, hour, minute, second, micro = 0;
|
||||
// YYYY-MM-DD
|
||||
if (!rtcParseDigits(s, 4, year) || *s++ != '-') return false;
|
||||
if (!rtcParseDigits(s, 2, month) || *s++ != '-') return false;
|
||||
if (!rtcParseDigits(s, 2, day)) return false;
|
||||
if (*s++ != 'T') return false;
|
||||
// HH:MM:SS
|
||||
if (!rtcParseDigits(s, 2, hour) || *s++ != ':') return false;
|
||||
if (!rtcParseDigits(s, 2, minute) || *s++ != ':') return false;
|
||||
if (!rtcParseDigits(s, 2, second)) return false;
|
||||
// Optional fractional seconds
|
||||
if (*s == '.') {
|
||||
s++;
|
||||
int frac = 0;
|
||||
int fracDigits = 0;
|
||||
while (*s >= '0' && *s <= '9' && fracDigits < 6) {
|
||||
frac = frac * 10 + (*s - '0');
|
||||
fracDigits++;
|
||||
s++;
|
||||
}
|
||||
// Scale to microseconds (6 digits)
|
||||
while (fracDigits < 6) { frac *= 10; fracDigits++; }
|
||||
while (fracDigits > 6) { frac /= 10; fracDigits--; }
|
||||
micro = frac;
|
||||
}
|
||||
// Timezone
|
||||
if (*s == 'Z') {
|
||||
s++;
|
||||
r.tzOffsetMinutes = 0;
|
||||
} else if (*s == '+' || *s == '-') {
|
||||
int tzSign = (*s == '+') ? 1 : -1;
|
||||
s++;
|
||||
int tzHour, tzMin;
|
||||
if (!rtcParseDigits(s, 2, tzHour)) return false;
|
||||
if (*s != ':') return false;
|
||||
s++;
|
||||
if (!rtcParseDigits(s, 2, tzMin)) return false;
|
||||
r.tzOffsetMinutes = tzSign * (tzHour * 60 + tzMin);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// Must consume entire string
|
||||
if (*s != '\0') return false;
|
||||
|
||||
r.date.year = year;
|
||||
r.date.month = month;
|
||||
r.date.day = day;
|
||||
r.date.hour = hour;
|
||||
r.date.minute = minute;
|
||||
r.date.second = second;
|
||||
r.date.microsecond = micro;
|
||||
r.ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool rtcParseRFC2822(const char *s, RtcParseResult &r) {
|
||||
r.ok = false;
|
||||
memset(&r.date, 0, sizeof(r.date));
|
||||
|
||||
// Optional weekday prefix: [weekday,]
|
||||
while (*s && *s != ' ' && *s != ',') s++;
|
||||
if (*s == ',') {
|
||||
s++;
|
||||
if (*s != ' ') return false;
|
||||
s++;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
int day, month, year, hour, minute, second;
|
||||
// DD
|
||||
if (!rtcParseDigits(s, 2, day)) return false;
|
||||
if (*s++ != ' ') return false;
|
||||
// Mon (3-letter month)
|
||||
if (!rtcParseMonthName(s, month)) return false;
|
||||
s += 3;
|
||||
if (*s++ != ' ') return false;
|
||||
// YYYY
|
||||
if (!rtcParseDigits(s, 4, year)) return false;
|
||||
if (*s++ != ' ') return false;
|
||||
// HH:MM:SS
|
||||
if (!rtcParseDigits(s, 2, hour) || *s++ != ':') return false;
|
||||
if (!rtcParseDigits(s, 2, minute) || *s++ != ':') return false;
|
||||
if (!rtcParseDigits(s, 2, second)) return false;
|
||||
if (*s++ != ' ') return false;
|
||||
// Timezone: ±HHMM
|
||||
if (*s != '+' && *s != '-') return false;
|
||||
int tzSign = (*s == '+') ? 1 : -1;
|
||||
s++;
|
||||
int tzHour, tzMin;
|
||||
if (!rtcParseDigits(s, 2, tzHour)) return false;
|
||||
if (!rtcParseDigits(s, 2, tzMin)) return false;
|
||||
r.tzOffsetMinutes = tzSign * (tzHour * 60 + tzMin);
|
||||
if (*s != '\0') return false;
|
||||
|
||||
r.date.year = year;
|
||||
r.date.month = month;
|
||||
r.date.day = day;
|
||||
r.date.hour = hour;
|
||||
r.date.minute = minute;
|
||||
r.date.second = second;
|
||||
r.ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int sceRtcParseDateTime(u32 destTickPtr, u32 dateStringPtr)
|
||||
{
|
||||
ERROR_LOG_REPORT(Log::sceRtc, "UNIMPL sceRtcParseDateTime(%d,%d)", destTickPtr, dateStringPtr);
|
||||
return 0;
|
||||
if (!Memory::IsValidAddress(destTickPtr) || !Memory::IsValidAddress(dateStringPtr))
|
||||
return hleLogError(Log::sceRtc, -1, "bad address");
|
||||
|
||||
const char *s = (const char *)Memory::GetPointer(dateStringPtr);
|
||||
if (!s) {
|
||||
return hleLogError(Log::sceRtc, -1, "null string");
|
||||
}
|
||||
|
||||
RtcParseResult r;
|
||||
memset(&r, 0, sizeof(r));
|
||||
|
||||
if (rtcParseRFC3339(s, r) || rtcParseRFC2822(s, r)) {
|
||||
u64 ticks = __RtcPspTimeToTicks(r.date);
|
||||
s64 offsetUs = (s64)r.tzOffsetMinutes * 60 * 1000000;
|
||||
ticks -= offsetUs;
|
||||
Memory::Write_U64(ticks, destTickPtr);
|
||||
return hleLogDebug(Log::sceRtc, 0);
|
||||
}
|
||||
|
||||
// Parse failed - return -1 without modifying destTickPtr.
|
||||
return hleLogDebug(Log::sceRtc, -1);
|
||||
}
|
||||
|
||||
static int sceRtcGetLastAdjustedTime(u32 tickPtr)
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# pspautotests
|
||||
|
||||
This test suite is located in the `pspautotests` submodule/subdirectory at the root of the repo. Its `tests/` subdirectory contains tests arranged by category (audio/, cpu/, gpu/, threads/, etc.).
|
||||
|
||||
The runner script `test.py` at the repo root contains two lists: `tests_good` (known-passing regression tests) and `tests_next` (tests that don't yet pass — move things here → `tests_good` by fixing PPSSPP).
|
||||
|
||||
Tests are compiled as PRX binaries (a variation on ELF) which PPSSPP can load. They also have an `.expected` file alongside them, containing the reference output from running on a real PSP via PSPLink. The goal is for PPSSPPHeadless's output to match.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Initialize the submodule** (if not done): `git submodule update --init`
|
||||
- The PRX binaries and `.expected` files are already committed inside the submodule — **no need to compile tests yourself** just to run them.
|
||||
- To build PPSSPPHeadless itself, see the next section.
|
||||
|
||||
## Building PPSSPPHeadless
|
||||
|
||||
### Windows (Visual Studio)
|
||||
|
||||
Open `Windows/PPSSPP.sln` in Visual Studio and build the **PPSSPPHeadless** project, or use MSBuild directly:
|
||||
|
||||
```
|
||||
MSBuild.exe -noAutoResponse Windows/PPSSPP.sln -target:PPSSPPHeadless -property:Configuration=Debug -property:Platform=x64 -maxCpuCount
|
||||
```
|
||||
|
||||
The built executable will be at `Windows/x64/Debug/PPSSPPHeadless.exe`.
|
||||
|
||||
> CMake-based builds do exist but are **not the recommended way** on Windows — stick with the VS solution.
|
||||
|
||||
### Linux / macOS
|
||||
|
||||
Use `./b.sh --debug`, then find the executable under `build/` (e.g. `build/Debug/PPSSPPHeadless`).
|
||||
|
||||
## Running tests
|
||||
|
||||
### Using test.py (recommended)
|
||||
|
||||
Make sure Python is available. On Windows the Microsoft Store alias may interfere; use `py` or the full path to `python.exe`.
|
||||
|
||||
- Run all good tests: `py test.py -g`
|
||||
- Run all broken/next tests: `py test.py -b`
|
||||
- Run all tests (good + next): `py test.py`
|
||||
- Run a specific test (or space-separated list): `py test.py -g cpu/cpu_alu/cpu_alu`
|
||||
- Run a group of tests by prefix match (add `-m`): `py test.py -g -m audio/atrac`
|
||||
|
||||
### Direct headless invocation
|
||||
|
||||
```
|
||||
Windows/x64/Debug/PPSSPPHeadless.exe --root pspautotests/tests/../ --compare --timeout=5 --graphics=software pspautotests/tests/audio/atrac/addstreamdata.prx
|
||||
```
|
||||
|
||||
Instead of a single PRX, you can pass a directory (e.g. `pspautotests/tests/threads/mbx/...`) to run all tests under it, recursively.
|
||||
|
||||
**Key flags:**
|
||||
- `--root` — points to the directory above `tests/` so the headless can find the expected directory layout.
|
||||
- `--compare` — enables output comparison against `.expected` files.
|
||||
- `--timeout=N` — seconds per test before killing it (default 5).
|
||||
- `--graphics=software` — uses software GPU backend (required for headless; no real GPU available).
|
||||
|
||||
### What you'll see
|
||||
|
||||
**Passing test:**
|
||||
```
|
||||
pspautotests/tests/audio/atrac/addstreamdata.prx:
|
||||
audio/atrac/addstreamdata - passed!
|
||||
1 tests passed, 0 tests failed, 0 tests missing.
|
||||
```
|
||||
|
||||
**Failing test:**
|
||||
```
|
||||
pspautotests/tests/threads/mbx/refer/refer.prx:
|
||||
O hi 0 prio=00 next=OTHER hi 1 prio=00 next=ITSELF ...
|
||||
E hi 1 prio=00 next=ITSELF ...
|
||||
+ ...
|
||||
0 tests passed, 1 tests failed, 0 tests missing.
|
||||
Failed tests:
|
||||
threads/mbx/refer/refer
|
||||
```
|
||||
|
||||
Lines prefixed with `O` are from the `.expected` file (real PSP), `E` is what PPSSPP produced, and `+` means a match.
|
||||
|
||||
The diff is line-by-line, so an `O` line followed by an `E` line at the same conceptual position means PPSSPP produced different output at that spot. A `+` line means both outputs agreed on that line.
|
||||
|
||||
## Workflow for fixing a test
|
||||
|
||||
1. Pick a test from `tests_next` in `test.py`.
|
||||
2. Run it with headless to confirm failure and see what differs (`O` vs `E` lines).
|
||||
3. Read the test source (`.c`/`.cpp`) and the `.expected` file to understand the API being tested.
|
||||
The `.expected` file was recorded from a real PSP — it's the ground truth. The test source
|
||||
reveals what syscalls are made and in what order. Sometimes the test deliberately corrupts
|
||||
state to probe kernel error handling.
|
||||
4. Form a hypothesis: look for a systematic pattern in the diffs (wrong order, wrong error
|
||||
code, missing output). Cross-reference with multiple expected files that exercise the same
|
||||
API — they may reveal the PSP's real behavior from different angles.
|
||||
5. Make changes to PPSSPP's HLE or other core code. Do not make super-targeted changes just
|
||||
to fix the test — instead, fix the underlying issue in a way that would also make sense
|
||||
on real PSP hardware.
|
||||
6. Rebuild PPSSPPHeadless and re-run the test.
|
||||
7. Run the full `tests_good` suite (`py test.py -g`) to check for regressions. A correct fix
|
||||
should not break any previously passing tests.
|
||||
8. Rinse and repeat until it passes, then move it from `tests_next` to `tests_good` in `test.py`.
|
||||
|
||||
### Tips from experience
|
||||
|
||||
- The diff output compares the full text output line-by-line. To see PPSSPP's raw output
|
||||
without the diff overlay, omit `--compare`:
|
||||
```
|
||||
Windows/x64/Debug/PPSSPPHeadless.exe --root pspautotests/tests/../ --timeout=5 --graphics=software path/to/test.prx
|
||||
```
|
||||
There's another trick too, --print-equal-lines, which prints matching lines with a '=' prefix, so you can see the full output with context.
|
||||
- Tests can show contradictory expected outputs at first glance. For example, the mbx/send
|
||||
test expected file shows FIFO message order (normal sends), while the mbx/refer test shows
|
||||
LIFO (after corruption) — because `sceKernelReferMbxStatus` on a real PSP *updates*
|
||||
`firstMessage` during traversal, changing the apparent head. Understanding the expected
|
||||
file's behavior often requires reading multiple related tests together.
|
||||
- When searching for the underlying issue, trace through the HLE implementation with the
|
||||
test's syscall sequence. Check whether the kernel writes into PSP-visible memory — if so,
|
||||
test code can corrupt those values, and the PSP kernel may have specific handling for that.
|
||||
- **Pointer addresses differ between PSP and PPSSPP.** A test that prints raw kernel pointers
|
||||
(heap addresses, TLS block addresses, etc.) will always have mismatched expected output
|
||||
because PSPLink shifts memory layout. Fix by changing the test to print offsets from a
|
||||
base address instead of absolute addresses. After changing the test, run it on hardware
|
||||
to generate a new `.expected`.
|
||||
- **The diff notation:**
|
||||
- `O` line = present in PPSSPP's output but not in expected (PPSSPP-only).
|
||||
- `E` line = present in expected but not in PPSSPP output (expected-only).
|
||||
- `+` line = context line (shown around diffs for context).
|
||||
- `=` line (with `--print-equal-lines`) = matching line.
|
||||
- `[r]` / `[x]` prefix = rescheduling occurred / did not occur since last line.
|
||||
- **When a function is a stub** (`UNIMPL` in log), the expected output is a good specification
|
||||
for what to implement. Look at multiple test cases in the expected file to understand the
|
||||
full range of valid and invalid inputs, return codes, and side effects.
|
||||
- **Time-dependent tests** (RTC, timezone conversions) depend on the host machine's clock
|
||||
and timezone. The PSP's `sceRtcParseDateTime` was a stub — the expected file showed
|
||||
exactly which RFC 3339 / RFC 2822 formats are accepted and which are rejected (return -1).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Python was not found" on Windows** — the Microsoft Store alias is interfering. Use `py` (the Python launcher) or the full path, e.g. `"C:/Users/.../AppData/Local/Programs/Python/Python314/python.exe" test.py -g`. Or disable the alias in Settings > Apps > Advanced app settings > App execution aliases.
|
||||
- **No PRX files found** — run `git submodule update --init` from the repo root to fetch the `pspautotests` submodule.
|
||||
- **PPSSPPHeadless exits immediately / "CPU not started"** — the test PRX may be missing or the `--root` path is wrong. Ensure `--root` points to the parent of `tests/`.
|
||||
- **MSBuild error MSB1008** — the `MSBuild.rsp` response file may be interfering. Always pass `-noAutoResponse` on Windows builds.
|
||||
+1
-1
Submodule pspautotests updated: 3b281cf05e...6ba9cf9e1e
@@ -234,6 +234,7 @@ tests_good = [
|
||||
"rtc/rtc",
|
||||
"rtc/arithmetic",
|
||||
"rtc/lookup",
|
||||
"rtc/convert",
|
||||
"string/string",
|
||||
"sysmem/freesize",
|
||||
"sysmem/memblock",
|
||||
@@ -283,6 +284,8 @@ tests_good = [
|
||||
"threads/mbx/poll/poll",
|
||||
"threads/mbx/priority/priority",
|
||||
"threads/mbx/receive/receive",
|
||||
"threads/mbx/refer/refer",
|
||||
"threads/mbx/send/send",
|
||||
"threads/msgpipe/msgpipe",
|
||||
"threads/msgpipe/cancel",
|
||||
"threads/msgpipe/create",
|
||||
@@ -328,6 +331,7 @@ tests_good = [
|
||||
"threads/threads/threads",
|
||||
"threads/tls/create",
|
||||
"threads/tls/delete",
|
||||
"threads/tls/get",
|
||||
"threads/tls/free",
|
||||
"threads/tls/priority",
|
||||
"threads/tls/refer",
|
||||
@@ -464,20 +468,17 @@ tests_next = [
|
||||
"net/http/http",
|
||||
"net/primary/ether",
|
||||
"power/freq",
|
||||
"rtc/convert",
|
||||
"sysmem/partition",
|
||||
"threads/callbacks/cancel",
|
||||
"threads/callbacks/count",
|
||||
"threads/callbacks/notify",
|
||||
# These two mbx tests only appeared to work because they papered over bugs
|
||||
"threads/mbx/refer/refer",
|
||||
"threads/mbx/send/send",
|
||||
|
||||
|
||||
"threads/scheduling/dispatch",
|
||||
"threads/scheduling/scheduling",
|
||||
"threads/threads/create",
|
||||
"threads/threads/terminate",
|
||||
"threads/tls/get",
|
||||
"threads/vpl/create",
|
||||
"umd/io/umd_io",
|
||||
"umd/raw_access/raw_access",
|
||||
|
||||
Reference in New Issue
Block a user