diff --git a/Common/Data/Random/Rng.h b/Common/Data/Random/Rng.h index 082053735f..b72c073124 100644 --- a/Common/Data/Random/Rng.h +++ b/Common/Data/Random/Rng.h @@ -36,24 +36,32 @@ public: }; // Data must consist only of the index and the twister array. This matches the PSP -// MT context exactly. +// MT context exactly - see pspautotests hash/mt19937ctx, which records the whole 2500 byte +// context off a real PSP: the counter sits at offset 0 and the 624 word array at offset 4, +// already twisted once by the time Init returns. class MersenneTwister { public: MersenneTwister(uint32_t seed) : index_(0) { mt_[0] = seed; for (uint32_t i = 1; i < MT_SIZE; i++) mt_[i] = (1812433253UL * (mt_[i - 1] ^ (mt_[i - 1] >> 30)) + i); + // The PSP twists during Init rather than lazily on the first draw, so a context that + // has been seeded but not yet used already holds the twisted array. + for (uint32_t i = 0; i < MT_SIZE; i++) + twist(i); } uint32_t R32() { - if (index_ == 0) - gen(); + // Read first, then regenerate that word in place - done in index order this is the + // same as twisting the whole array in one go, and it matches what hardware leaves + // behind in the context after each draw. uint32_t y = mt_[index_]; + twist(index_); + index_ = (index_ + 1) % MT_SIZE; y ^= y >> 11; y ^= (y << 7) & 2636928640UL; y ^= (y << 15) & 4022730752UL; y ^= y >> 18; - index_ = (index_ + 1) % MT_SIZE; return y; } @@ -65,12 +73,11 @@ private: u32_le index_; u32_le mt_[MT_SIZE]; - void gen() { - for(uint32_t i = 0; i < MT_SIZE; i++){ - uint32_t y = (mt_[i] & 0x80000000) + (mt_[(i + 1) % MT_SIZE] & 0x80000000); - mt_[i] = mt_[(i + 397) % MT_SIZE] ^ (y >> 1); - if (y % 2) mt_[i] ^= 2567483615UL; - } - return; + void twist(uint32_t i) { + // The low half has to come through the LOWER mask - masking both halves with + // 0x80000000 makes this something other than MT19937, which is what it used to do. + uint32_t y = (mt_[i] & 0x80000000) | (mt_[(i + 1) % MT_SIZE] & 0x7FFFFFFF); + mt_[i] = mt_[(i + 397) % MT_SIZE] ^ (y >> 1); + if (y & 1) mt_[i] = mt_[i] ^ 2567483615UL; } }; diff --git a/Core/HLE/sceKernelMemory.cpp b/Core/HLE/sceKernelMemory.cpp index c89fe80214..ac0ec377ff 100644 --- a/Core/HLE/sceKernelMemory.cpp +++ b/Core/HLE/sceKernelMemory.cpp @@ -390,11 +390,17 @@ void __KernelMemoryShutdown() } BlockAllocator *BlockAllocatorFromID(int id) { + // A kernel module gets the privileged partitions whichever entry point it came in through - + // threads/tls/kernel/partition records sceKernelCreateTlspl accepting 1, 3 and 4 from a + // kernel module and refusing them from user mode, and it reaches the kernel through the + // ordinary ThreadManForUser NID either way. hleIsKernelMode() alone only catches the case + // where the export itself is kernel-only. + const bool kernelMode = hleIsKernelMode() || __KernelCurThreadIsKernelMode(); switch (id) { case KERNEL_PARTITION_ID: case 3: case 4: - if (hleIsKernelMode()) + if (kernelMode) return &kernelMemory; return nullptr; @@ -404,7 +410,7 @@ BlockAllocator *BlockAllocatorFromID(int id) { case 8: case 10: - if (hleIsKernelMode()) + if (kernelMode) return &userMemory; return nullptr; @@ -1863,7 +1869,12 @@ SceUID sceKernelCreateTlspl(const char *name, u32 partition, u32 attr, u32 block return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_NO_MEMORY, "invalid name"); if ((attr & ~PSP_TLSPL_ATTR_KNOWN) >= 0x100) return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ATTR, "invalid attr parameter: %08x", attr); - if (partition < 1 || partition > 9 || partition == 7) + // Only 1-6 exist, in either privilege level: threads/tls/partition and its kernel-mode twin + // both record 7 and up returning ILLEGAL_ARGUMENT on a real PSP. What the privilege changes + // is the permission check below - 1, 3 and 4 are ILLEGAL_PERM from user mode and fine from + // kernel mode. Note this differs from sceKernelCreateVpl above, which lets 8 and 9 reach the + // permission check; the two used to share a range that was only ever right for Vpl. + if (partition < 1 || partition > 6) return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ARGUMENT, "invalid partition %d", partition); BlockAllocator *allocator = BlockAllocatorFromID(partition); diff --git a/Core/HLE/sceKernelThread.cpp b/Core/HLE/sceKernelThread.cpp index bd139eede5..260c349f61 100644 --- a/Core/HLE/sceKernelThread.cpp +++ b/Core/HLE/sceKernelThread.cpp @@ -2290,6 +2290,17 @@ SceUID __KernelGetCurThreadModuleId() { return 0; } +bool __KernelCurThreadIsKernelMode() { + PSPThread *thread = __GetCurrentThread(); + if (thread && (thread->nt.attr & PSP_THREAD_ATTR_KERNEL) != 0) { + return true; + } + // A kernel module's own main thread isn't necessarily flagged kernel - the attribute comes + // from PSP_MAIN_THREAD_ATTR in the module info, which needn't set it - so fall back to what + // module the thread belongs to, the same way sceKernelCreateThread decides allowKernel. + return KernelModuleIsKernelMode(__KernelGetCurThreadModuleId()); +} + u32 __KernelGetCurThreadStack() { PSPThread *t = __GetCurrentThread(); if (t) diff --git a/Core/HLE/sceKernelThread.h b/Core/HLE/sceKernelThread.h index 146c3bb5f5..2edcff09fe 100644 --- a/Core/HLE/sceKernelThread.h +++ b/Core/HLE/sceKernelThread.h @@ -341,6 +341,10 @@ KernelObject *__KernelCallbackObject(); SceUID __KernelGetCurThread(); int KernelCurThreadPriority(); bool KernelChangeThreadPriority(SceUID threadID, int priority); +// Whether the running thread belongs to a kernel module. Privilege on the PSP is a property of +// the caller, not of the syscall - hleIsKernelMode() only says the entry point itself is a +// kernel-only export, which is a different question. +bool __KernelCurThreadIsKernelMode(); u32 __KernelGetCurThreadStack(); u32 __KernelGetCurThreadStackStart(); const char *__KernelGetThreadName(SceUID threadID); diff --git a/Core/HLE/sceMd5.cpp b/Core/HLE/sceMd5.cpp index 6653ae9e92..7f014d864b 100644 --- a/Core/HLE/sceMd5.cpp +++ b/Core/HLE/sceMd5.cpp @@ -48,9 +48,103 @@ u32 sceKernelUtilsMt19937UInt(u32 ctx) { return mt->R32(); } -// TODO: This MD5 stuff needs tests! +// The MD5 context lives in the game's own memory, exactly as it does on hardware, so two digests +// can be in flight at once. Layout confirmed against a real PSP by pspautotests hash/md5ctx: +// 96 bytes, and the word at offset 16 is never written by the kernel. +// +// SHA-1 gets the same treatment further down. Its context is the same size with the same +// bookkeeping, but note it does not stream whole blocks through buf the way MD5 does. +struct PSPMd5Context { + u32_le h[4]; + u32_le pad; // the kernel leaves this one alone + u16_le usRemains; // bytes currently held in buf + u16_le usComputed; // stays zero on hardware + u64_le ullTotalLen; // total bytes fed in so far + u8 buf[64]; +}; + +static void Md5ContextRead(const PSPPointer &ctx, md5_context *out) { + for (int i = 0; i < 4; i++) { + out->state[i] = ctx->h[i]; + } + u64 total = ctx->ullTotalLen; + out->total[0] = (u32)total; + out->total[1] = (u32)(total >> 32); + memcpy(out->buffer, ctx->buf, sizeof(out->buffer)); +} + +static void Md5ContextWrite(PSPPointer &ctx, const md5_context *in) { + for (int i = 0; i < 4; i++) { + ctx->h[i] = (u32)in->state[i]; + } + u64 total = (u64)(u32)in->total[0] | ((u64)(u32)in->total[1] << 32); + ctx->ullTotalLen = total; + ctx->usRemains = (u16)(total & 0x3F); + ctx->usComputed = 0; + ctx.NotifyWrite("Md5Context"); +} + +// Hardware streams every byte through buf on its way into the digest, so after a whole-block +// update buf holds that block - not just the leftover tail. Our md5 hashes full blocks straight +// out of the caller's buffer, so reproduce what the PSP would have left behind: the data laid +// into a 64 byte window at its absolute offset in the stream. +static void Md5ContextFillBuf(PSPPointer &ctx, u64 totalBefore, const u8 *data, u32 len) { + if (len == 0) { + return; + } + if (len >= 64) { + data += len - 64; + totalBefore += len - 64; + len = 64; + } + for (u32 i = 0; i < len; i++) { + ctx->buf[(u32)((totalBefore + i) % 64)] = data[i]; + } + ctx.NotifyWrite("Md5Context"); +} + +// Init touches only the state and the counters - buf and the pad word are left as they were. +static int Md5BlockInit(u32 ctxAddr) { + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid()) + return hleLogError(Log::HLE, -1, "bad context address"); + md5_context fresh; + ppsspp_md5_starts(&fresh); + for (int i = 0; i < 4; i++) { + ctx->h[i] = (u32)fresh.state[i]; + } + ctx->usRemains = 0; + ctx->usComputed = 0; + ctx->ullTotalLen = 0; + ctx.NotifyWrite("Md5Context"); + return hleLogDebug(Log::HLE, 0); +} + +static int Md5BlockUpdate(u32 ctxAddr, u32 dataPtr, u32 len) { + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid() || !Memory::IsValidRange(dataPtr, len)) + return hleLogError(Log::HLE, -1, "bad address"); + md5_context work; + Md5ContextRead(ctx, &work); + u64 totalBefore = ctx->ullTotalLen; + const u8 *data = Memory::GetPointerWriteUnchecked(dataPtr); + ppsspp_md5_update(&work, (unsigned char *)data, (int)len); + Md5ContextWrite(ctx, &work); + Md5ContextFillBuf(ctx, totalBefore, data, len); + return hleLogDebug(Log::HLE, 0); +} + +static int Md5BlockResult(u32 ctxAddr, u32 digestAddr) { + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid() || !Memory::IsValidRange(digestAddr, 16)) + return hleLogError(Log::HLE, -1, "bad address"); + md5_context work; + Md5ContextRead(ctx, &work); + ppsspp_md5_finish(&work, Memory::GetPointerWriteUnchecked(digestAddr)); + Md5ContextWrite(ctx, &work); + return hleLogDebug(Log::HLE, 0); +} -static md5_context md5_ctx; static int sceMd5Digest(u32 dataAddr, u32 len, u32 digestAddr) { DEBUG_LOG(Log::HLE, "sceMd5Digest(%08x, %d, %08x)", dataAddr, len, digestAddr); @@ -63,33 +157,15 @@ static int sceMd5Digest(u32 dataAddr, u32 len, u32 digestAddr) { } static int sceMd5BlockInit(u32 ctxAddr) { - DEBUG_LOG(Log::HLE, "sceMd5BlockInit(%08x)", ctxAddr); - if (!Memory::IsValidAddress(ctxAddr)) - return -1; - - // TODO: Until I know how large a context is, we just go all lazy and use a global context, - // which will work just fine unless games do several MD5 concurrently. - - ppsspp_md5_starts(&md5_ctx); - return 0; + return Md5BlockInit(ctxAddr); } static int sceMd5BlockUpdate(u32 ctxAddr, u32 dataPtr, u32 len) { - DEBUG_LOG(Log::HLE, "sceMd5BlockUpdate(%08x, %08x, %d)", ctxAddr, dataPtr, len); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(dataPtr)) - return -1; - - ppsspp_md5_update(&md5_ctx, Memory::GetPointerWriteUnchecked(dataPtr), (int)len); - return 0; + return Md5BlockUpdate(ctxAddr, dataPtr, len); } static int sceMd5BlockResult(u32 ctxAddr, u32 digestAddr) { - DEBUG_LOG(Log::HLE, "sceMd5BlockResult(%08x, %08x)", ctxAddr, digestAddr); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(digestAddr)) - return -1; - - ppsspp_md5_finish(&md5_ctx, Memory::GetPointerWriteUnchecked(digestAddr)); - return 0; + return Md5BlockResult(ctxAddr, digestAddr); } int sceKernelUtilsMd5Digest(u32 dataAddr, int len, u32 digestAddr) { @@ -103,37 +179,67 @@ int sceKernelUtilsMd5Digest(u32 dataAddr, int len, u32 digestAddr) { } int sceKernelUtilsMd5BlockInit(u32 ctxAddr) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsMd5BlockInit(%08x)", ctxAddr); - if (!Memory::IsValidAddress(ctxAddr)) - return -1; - - // TODO: Until I know how large a context is, we just go all lazy and use a global context, - // which will work just fine unless games do several MD5 concurrently. - - ppsspp_md5_starts(&md5_ctx); - return 0; + return Md5BlockInit(ctxAddr); } int sceKernelUtilsMd5BlockUpdate(u32 ctxAddr, u32 dataPtr, int len) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsMd5BlockUpdate(%08x, %08x, %d)", ctxAddr, dataPtr, len); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(dataPtr)) - return -1; - - ppsspp_md5_update(&md5_ctx, Memory::GetPointerWriteUnchecked(dataPtr), (int)len); - return 0; + return Md5BlockUpdate(ctxAddr, dataPtr, (u32)len); } int sceKernelUtilsMd5BlockResult(u32 ctxAddr, u32 digestAddr) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsMd5BlockResult(%08x, %08x)", ctxAddr, digestAddr); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(digestAddr)) - return -1; - - ppsspp_md5_finish(&md5_ctx, Memory::GetPointerWriteUnchecked(digestAddr)); - return 0; + return Md5BlockResult(ctxAddr, digestAddr); } -static sha1_context sha1_ctx; +// SHA-1's context, confirmed against a real PSP by pspautotests hash/sha1ctx. Same 96 bytes and +// same bookkeeping as MD5, but no pad word - and unlike MD5, a whole-block update leaves buf +// alone rather than copying the block through it, which is what our sha1 does anyway. +struct PSPSha1Context { + u32_le h[5]; + u16_le usRemains; + u16_le usComputed; + u64_le ullTotalLen; + u8 buf[64]; +}; + +static void Sha1ContextRead(const PSPPointer &ctx, sha1_context *out) { + for (int i = 0; i < 5; i++) { + out->state[i] = ctx->h[i]; + } + u64 total = ctx->ullTotalLen; + out->total[0] = (u32)total; + out->total[1] = (u32)(total >> 32); + memcpy(out->buffer, ctx->buf, sizeof(out->buffer)); +} + +static void Sha1ContextWrite(PSPPointer &ctx, const sha1_context *in) { + for (int i = 0; i < 5; i++) { + ctx->h[i] = (u32)in->state[i]; + } + u64 total = (u64)(u32)in->total[0] | ((u64)(u32)in->total[1] << 32); + ctx->ullTotalLen = total; + ctx->usRemains = (u16)(total & 0x3F); + ctx->usComputed = 0; + memcpy(ctx->buf, in->buffer, sizeof(ctx->buf)); + ctx.NotifyWrite("Sha1Context"); +} + +static int Sha1BlockInit(u32 ctxAddr) { + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid()) + return hleLogError(Log::HLE, -1, "bad context address"); + sha1_context fresh; + sha1_starts(&fresh); + for (int i = 0; i < 5; i++) { + ctx->h[i] = (u32)fresh.state[i]; + } + ctx->usRemains = 0; + ctx->usComputed = 0; + ctx->ullTotalLen = 0; + ctx.NotifyWrite("Sha1Context"); + return hleLogDebug(Log::HLE, 0); +} + int sceKernelUtilsSha1Digest(u32 dataAddr, int len, u32 digestAddr) { DEBUG_LOG(Log::HLE, "sceKernelUtilsSha1Digest(%08x, %d, %08x)", dataAddr, len, digestAddr); @@ -146,34 +252,29 @@ int sceKernelUtilsSha1Digest(u32 dataAddr, int len, u32 digestAddr) { } int sceKernelUtilsSha1BlockInit(u32 ctxAddr) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsSha1BlockInit(%08x)", ctxAddr); - if (!Memory::IsValidAddress(ctxAddr)) - return -1; - - // TODO: Until I know how large a context is, we just go all lazy and use a global context, - // which will work just fine unless games do several MD5 concurrently. - - sha1_starts(&sha1_ctx); - - return 0; + return Sha1BlockInit(ctxAddr); } int sceKernelUtilsSha1BlockUpdate(u32 ctxAddr, u32 dataAddr, int len) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsSha1BlockUpdate(%08x, %08x, %d)", ctxAddr, dataAddr, len); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(dataAddr)) - return -1; - - sha1_update(&sha1_ctx, Memory::GetPointerWriteUnchecked(dataAddr), (int)len); - return 0; + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid() || !Memory::IsValidRange(dataAddr, len)) + return hleLogError(Log::HLE, -1, "bad address"); + sha1_context work; + Sha1ContextRead(ctx, &work); + sha1_update(&work, Memory::GetPointerWriteUnchecked(dataAddr), (int)len); + Sha1ContextWrite(ctx, &work); + return hleLogDebug(Log::HLE, 0); } int sceKernelUtilsSha1BlockResult(u32 ctxAddr, u32 digestAddr) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsSha1BlockResult(%08x, %08x)", ctxAddr, digestAddr); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(digestAddr)) - return -1; - - sha1_finish(&sha1_ctx, Memory::GetPointerWriteUnchecked(digestAddr)); - return 0; + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid() || !Memory::IsValidRange(digestAddr, 20)) + return hleLogError(Log::HLE, -1, "bad address"); + sha1_context work; + Sha1ContextRead(ctx, &work); + sha1_finish(&work, Memory::GetPointerWriteUnchecked(digestAddr)); + Sha1ContextWrite(ctx, &work); + return hleLogDebug(Log::HLE, 0); } diff --git a/SDL/CocoaBarItems.mm b/SDL/CocoaBarItems.mm index aedfdcb271..0306d2b915 100644 --- a/SDL/CocoaBarItems.mm +++ b/SDL/CocoaBarItems.mm @@ -267,6 +267,13 @@ void OSXOpenURL(const char *url) { openMemstickFolderItem.target = self; [self.fileMenu addItem:openMemstickFolderItem]; + [self.fileMenu addItem:[NSMenuItem separatorItem]]; + + NSMenuItem *loadVSH = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Load VSH") action:@selector(loadVSH) keyEquivalent:@""]; + loadVSH.enabled = YES; + loadVSH.target = self; + [self.fileMenu addItem:loadVSH]; + return menu; } @@ -448,12 +455,12 @@ void OSXOpenURL(const char *url) { } -(void)pauseAction: (NSMenuItem *)item { - System_PostUIMessage(UIMessage::REQUEST_GAME_PAUSE); + System_PostUIMessage(UIMessage::REQUEST_GAME_PAUSE); } -(void)resetAction: (NSMenuItem *)item { System_PostUIMessage(UIMessage::REQUEST_GAME_RESET); - Core_Resume(); + Core_Resume(); } -(void)chatAction: (NSMenuItem *)item { @@ -494,7 +501,7 @@ void OSXOpenURL(const char *url) { } -(void)saveFrameDump { - System_PostUIMessage(UIMessage::SAVE_FRAME_DUMP); + System_PostUIMessage(UIMessage::SAVE_FRAME_DUMP); } -(void)takeScreenshot { @@ -657,6 +664,10 @@ TOGGLE_METHOD(FullScreen, g_Config.bFullScreen, g_Config.bFullScreen = !g_Config [NSWorkspace.sharedWorkspace openURL:memstickURL]; } +-(void)loadVSH { + System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, (g_Config.nandRootDirectory / "flash0/vsh/module/vshmain.prx").ToString()); +} + - (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:self]; } diff --git a/docs/pspautotests-hardware.md b/docs/pspautotests-hardware.md index 7597c2480b..67329f07a4 100644 --- a/docs/pspautotests-hardware.md +++ b/docs/pspautotests-hardware.md @@ -174,6 +174,60 @@ python3 test.py --graphics=software io/shortname/shortname - **The module must be named `TESTMODULE`** (`common.c` does this) or `gentest.py` prints the load line as an unexpected result. +## Kernel-mode tests + +Some behaviour differs by privilege - `sceKernelCreateTlspl` accepts partitions 1, 3 and 4 from a +kernel module and rejects them with `ILLEGAL_PERM` from user mode - so occasionally a test has to +run as a kernel module. `tests/threads/tls/kernel` is the working example. Set `COMMON_KERNEL = 1` +in the Makefile before including `common.mk` and you get a kernel build: + +```make +TARGETS = partition +EXTRA_OBJS = tlspl-imports.o +COMMON_KERNEL = 1 + +COMMON_DIR = ../../../../common +include $(COMMON_DIR)/common.mk +``` + +Three things make this work, and all three took a while to find, so don't undo them casually: + +- **`USE_KERNEL_LIBS`, set automatically by `common.mk`.** The stock `crt0_prx.o` references + `__libcglue_init`, which pulls in the whole of libcglue, which imports `sceNetInet`, + `sceUtility` and the `ForUser` IO libraries. A kernel module that imports any of those fails to + load with `8002013C` (library not found), and no amount of trimming `LIBS` helps because the + reference comes from the startup object itself. `USE_KERNEL_LIBS` selects a different startup - + and brings `-nostdlib`, so `common/kernelglue.c` supplies the newlib support hooks that go + missing: `_sbrk` over a fixed heap, the `_read`/`_write`/`_close` stubs, no-op retargetable + locks, and `module_start`. +- **Nothing may import a `ForUser` library.** `ThreadManForUser` is fine, but `SysMemUserForUser` + is not, which is why `common.c` compiles the `sceKernelSetCompiledSdkVersion` helpers out of + kernel builds - a function pointer table referencing them is enough to pull the stubs in. + Watch for this: `psp-strings yourtest.prx | grep For` lists what you're importing. Beware also + that `ThreadManForKernel` does not export the Tlspl calls - importing them from there links + cleanly and then returns `8002013A`, library not yet linked, from every call. +- **Size.** The kernel partition has about 285 KB free with PSPLink resident, 242 KB of it + contiguous, and loading over the cable needs room for the file *and* the loaded image at once. + A kernel build therefore gets a 4 KB `schedfBuffer` and a 4 KB heap instead of the user build's + 64 KB and 21 MB. `psp-size yourtest.elf` plus the `.prx` file size against `meminfo`'s MAXFREE + tells you whether it will fit; `800200D9` (memblock alloc failed) means it didn't. + +Output works normally - `printf`, `checkpoint` and `schedf` all behave - but a kernel build writes +to `host0:` with `sceIo` directly rather than through newlib's `FILE`, since that's what dragged +libcglue in. Formatting still goes through newlib's `vsnprintf`, so the usual format specifiers +are all available. + +Output reaches an emulator the same way it reaches the cable: a kernel build feeds +`sceIoDevctl("emulator:", SEND_OUTPUT)` as well as writing the file, and calls +`sceKernelExitGame` at the end so headless stops rather than spinning to its timeout. Both are +easy to forget when writing a new harness - without the first the test appears to produce nothing, +and without the second it always reports TIMEOUT even though it ran. + +One emulator-side note, since it took a while to pin down: privilege on the PSP belongs to the +*caller*, not to the syscall. PPSSPP's `hleIsKernelMode()` only reports whether the entry point +itself is a kernel-only export, so a kernel module calling an ordinary `ForUser` NID used to look +like user mode. `__KernelCurThreadIsKernelMode()` answers the question this test needs. + ## Worked example: FAT short names `tests/io/shortname` was written this way and is a decent template. It creates a scratch directory diff --git a/pspautotests b/pspautotests index 40317c54ce..7c5d150d40 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 40317c54ce6709b0f52e7617e01fbaff03806a19 +Subproject commit 7c5d150d40ab585d8bc1b5f60f5673eb56620ac9 diff --git a/test.py b/test.py index 951a7195ca..5caaed33a2 100755 --- a/test.py +++ b/test.py @@ -210,6 +210,9 @@ tests_good = [ "gpu/vertices/morph", # "gpu/vertices/texcoords", # See issue #19093 "hash/hash", + "hash/md5ctx", + "hash/mt19937ctx", + "hash/sha1ctx", "hle/check_not_used_uids", "intr/intr", "intr/enablesub", @@ -336,6 +339,9 @@ tests_good = [ "threads/threads/threadmanidlist", "threads/threads/threadmanidtype", "threads/threads/threads", + "threads/tls/create", + "threads/tls/partition", + "threads/tls/kernel/partition", "threads/tls/delete", "threads/tls/get", "threads/tls/free", @@ -456,6 +462,7 @@ tests_next = [ "io/io/io", "io/iodrv/iodrv", "io/shortname/shortname", + "io/stat/stat", "io/open/tty0", "jpeg/csc", "jpeg/decode", @@ -481,7 +488,7 @@ tests_next = [ "threads/scheduling/scheduling", "threads/threads/create", "threads/threads/terminate", - "threads/tls/create", + "threads/tls/memory", "threads/vpl/create", "umd/io/umd_io", "umd/raw_access/raw_access",