Semaphores, event flags, mutexes, lwmutexes, mbx, msgpipes, fpl, vpl,
tlspl and WaitThreadEnd each had their own CoreTiming event, handler
registration and savestate entry for wait timeouts, and their own function
to schedule one. Now one event (WaitThreadEnd's, renamed) times out all of
them, keyed by thread, and dispatches on the thread's wait type to a
timeoutFunc registered alongside the begin/end callback functions.
__KernelWaitCurThreadWithTimeout() starts such a wait, and the HLEKernel
helpers have overloads that use the shared event.
Old savestates still load: each object's section reads its old event id
and points it at the shared handler, so a timeout pending in the state
goes off as before. Checked with a state saved mid-wait by the previous
build, and with four games.
The one behaviour change: tlspl timeouts now follow the same hardware
rule as the others, where they used the raw timeout.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Every wait with a timeout behaves the same on hardware (pspautotests
threads/scheduling/waittimeouts). The deadline is taken, and the alarm set
up a moment later. If the deadline has passed by then, the wait fails with
WAIT_TIMEOUT at once, without yielding or writing the timeout back. That's
usual for 0us, half the time for 1us, and rare after; AllocateVpl does more
first. Otherwise it ends max(t, 205us) + ~35us after the call. Each object
had its own guess (24/245, 25/250, 20/250 and so on), and only MsgPipe had
the immediate case.
__KernelWaitTimesOutAtOnce() and __KernelWaitTimeoutUs() now do it for
semaphores, event flags, mutexes, lwmutexes, mbx, msgpipes, fpl, vpl and
WaitThreadEnd. The latency past the deadline isn't counted in the time
left written back.
Outcomes that hardware decides by the clock's phase (these, and
sceKernelDelayThread returning at once) go with the likelier one. Ones
between 50% and certain are instead spread evenly over calls, so a polling
loop can't lock into never yielding (sceKernelThread section version 7).
This replaces the pseudo-random choice for delays.
Also adds threads/scheduling/readyqueue, which already passes.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Verified against pspautotests threads/callbacks/waittypes:
- __KernelThreadingInit() cleared the wait type callback table after
__KernelMemoryInit() had registered VPL and FPL in it, so a VPL or FPL
wait interrupted by a callback was never paused or resumed, and could
hang forever.
- A msgpipe deleted during a callback left its waiter waiting, instead
of waking it with WAIT_DELETE.
- A wait that got its object during a callback reported no time left;
put the timer back before trying to unlock, so the unlock writes what
remains.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The new sysmem tests run the same partition sweep from both privilege levels, which settles
several things that were guesses:
The valid range is 1-6, not 1-9-except-7. sceKernelCreateVpl, CreateFpl, CreateMsgPipe and
AllocPartitionMemory all let 8 and 9 through to the permission check, so a caller asking for
partition 8 got ILLEGAL_PERM where hardware says ILLEGAL_ARGUMENT. Privilege changes the
permission check, not the range - 1, 3 and 4 are refused from user mode and work from kernel mode
in every one of these APIs, which is the evidence the earlier BlockAllocatorFromID change was
missing.
sceKernelAllocPartitionMemory reports an out-of-range partition differently depending on which
entry point was used - ILLEGAL_ARGUMENT through SysMemUserForUser, ILLEGAL_PARTITION through
SysMemForKernel. Both NIDs land on the same function here, and hleIsKernelMode() is precisely
"came in through the kernel NID", so it picks the right one.
sceKernelCreateHeap had four "TODO: Validate error code" comments and no test at all - it's
kernel-only, which is why. All four are now recorded: out-of-range partitions are
ILLEGAL_PARTITION, a size of zero or less is HEAPBLOCK_ALLOC_FAILED before anything is allocated,
a NULL name is refused with ERROR, and flags really are ignored. sceKernelAllocHeapMemoryWithOption
had its validation backwards: the option struct's size field isn't checked at all, while the
alignment must be a power of two from 4 to 0x80.
Not fixed, and split into sysmem/kernel/heapgrow in tests_next: a real heap will hand out a block
larger than the heap itself, so the size isn't a cap. Ours is a fixed allocator over the reserved
block. Worth establishing how far the real one grows before implementing that.
Risk: the range change makes partitions 8 and 9 fail earlier and with a different code than
before. Nothing in tests_good depended on the old behaviour except two expectations that had
drifted from hardware, corrected in the submodule.
CoreTiming::UnscheduleEvent returns the scheduled time minus the current time,
which is negative when the event is overdue but hasn't been processed yet - the
exact situation when a wait is satisfied right around its own timeout. Only the
semaphore clamped it; everywhere else we wrote (u32)cyclesToUs(negative) into
the game's timeout variable, i.e. a huge bogus "remaining time".
Pulled the shared shape into HLEKernel::WriteRemainingTimeout so it can't drift
apart again - event flags, mbx, fpl, vpl, msgpipe, mutex, lwmutex and semaphore
all go through it now. The two thread-end sites keep their own copy since they
unschedule even when the game passed no timeout pointer, and sceUsb just gets
the clamp.
These KernelObject subclasses (and their Native* status structs) were
private implementation details of their respective .cpp files. Move them
into the matching .h instead, so external code - specifically the upcoming
WebSocket kernel-object introspection endpoints - can read a live object's
state directly via kernelObjects.Get<T>()/Iterate<T>(), the same way
PSPModule/PSPThread already can. Read-only by convention: nothing outside
each file should call DoState() or otherwise mutate these; the fields are
public here for that file's own pre-existing use, not an invitation to
write from elsewhere.
To avoid pulling each type's full dependency set (Memory::, BlockAllocator,
CoreTiming, HLEKernel::...) into headers many other files include, non-trivial
method bodies (DoState, and MsgPipe's buffer/wait-list management) are
declared in the header but still defined out-of-line in the .cpp, same as
before - only genuinely trivial one-liners went inline.
KernelObjectPool also gains IterateAll(), a type-agnostic sibling of the
existing Iterate<T>() - walks every live kernel object regardless of type,
for a coarse "what's alive right now" overview.
No behavior change - this is a pure visibility/declaration-vs-definition
move, not new functionality. That lands in a follow-up commit.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
* Rename LogType to Log
* Explicitly use the Log:: enum when logging. Allows for autocomplete when editing.
* Mac/ARM64 buildfix
* Do the same with the hle result log macros
* Rename the log names to mixed case while at it.
* iOS buildfix
* Qt buildfix attempt, ARM32 buildfix
Probably not super important, but makes tests happier.
Also, when unscheduling an event, return the current time left, including
already spent time since last Advance.