Headless replays frame dumps to compare them with references and with the PSP, which a game's
workarounds skew: DarkStalkers' present hack (16510) replaces the game's final upscale, so the
replay didn't match the PSP's. The app still applies them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--loglevel, -d and -v are applied to the channels directly, and the next
config save wrote them to [Log], permanently replacing the user's levels.
The other command line options avoid this with DoNotSaveSetting.
The debugger had the same problem and a flag for it. Generalize that one
(NotifyChannelsChangedByDebugger -> DoNotSaveChannels) and set it here too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With the software renderer, which now stores depth as the GE does, a
16-bit read or write at VRAM's 0x04200000 or 0x04600000 mirror goes to
where the GE's 16-bit or 32-bit color layout puts that depth pixel
(ppsspp-re geprobe exp88: the mirrors are the layout's exact inverse).
Games access depth there with nothing else, so other widths stay plain.
The accessors check the address, then whether the GPU core is the
software renderer (CoreParameter); the hardware renderers keep depth plain
and handle the layout their own way. The renderer keeps the layouts up to
date with the EDRAM address translation. With it, the JITs send 16-bit
loads and stores to the interpreter, and the IR passes don't change a
load's width to or from 16 bits.
The pspautotests that read depth through the mirrors now do so 16 bits
at a time, re-recorded on hardware unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stage 5 of the JavaGL removal. Now that the GL path owns its own EGL context and
render thread, Android's two lifecycle branches and the Win32 one in
MainThreadFunc were the same code three times over, so this moves the branch into
one place - RunGraphicsLoop() in Core/EmuThread.cpp. It picks the thread
arrangement the context asks for: either this thread becomes the render thread
and spawns the emu thread (OpenGL), or it just runs the main loop because the
backend spawns its own render thread (Vulkan, D3D11). Android, Windows and
headless all go through it now, which is what the "TODO: Merge with the Win32
EmuThread and so on" in app-android.cpp was asking for.
Falls out of having one implementation instead of three:
- MainThreadFunc called graphicsContext->ThreadStart() right after
EmuThread_Start(), which already calls it, and ThreadEnd() right after
EmuThread_Join(), which already calls that too. So Windows and headless GL ran
CreateDeviceObjects()/DestroyDeviceObjects() twice per context, leaking a VAO
on the way in and double-deleting it on the way out. Dates back to 16206d18ab,
which moved those calls into EmuThread_Start/Join without removing the
originals.
- RECREATE_VIEWS is gone. Only the GL path ever posted it; Vulkan goes through
the same destroy/recreate without it and always has. Verified on device: views
survive pause/resume, and both a real rotation and a density change - each one
an activity destroy and recreate - lay them out again by themselves.
- The non-separate path is now named EmuThread rather than MainThread on
Windows. It's the thread that runs NativeFrame(), which is what
DebuggerThreading.md calls the emu thread, and the real main thread there is
the GUI message pump.
The loop keeps an explicit exit flag, as a shouldExit callback, rather than
relying on ThreadFrame() returning false. In VR we skip ThreadFrame() entirely
while the XR session is idle, and the session going idle is exactly what
backgrounding the app does - so the exit would never be observed, the render
thread would sit in that sleep forever, and the UI thread joining it would hang.
That is not hypothetical: it deadlocked a Quest 2 on the first try.
Also drops a dead EmuThreadState enum and an unused JNIEnv local in
app-android.cpp, both orphaned when the JavaGL JNI entry points went away.
Tested on a nubia Z2464N (Mali-G57, Android 15) on both backends: cold start,
booting a homebrew, pause/resume in the menu, in a settings screen and in-game,
rotation back and forth, a density-change activity recreate with the game live,
the pause menu, and exit to menu. ThreadStart/ThreadEnd log exactly once per
context, and every one of those cycles tears the EGL context down and builds a
new one.
Also tested on a Quest 2 with the vr flavor, which is what shook out the deadlock
above: cold start into a focused XR session at 72fps, backgrounding (render
thread joins in 27ms where it used to never return), resuming back into VR with
a fresh EGL context, and exiting from in-app. xrCreateSession runs on the render
loop thread, which is where the EGL context is current - VRBase.cpp builds its
graphics binding from eglGetCurrentDisplay/eglGetCurrentContext, so that ordering
matters.
Windows x64 debug and the Android gradle builds are clean.
Not converged, and not a small change: NeedsSeparateEmuThread() itself. GL needs
its API calls on the thread its context is current on, so somebody has to pump
ThreadFrame() there, whereas VulkanRenderManager owns its thread outright.
Getting rid of the virtual means teaching GLRenderManager to spawn and own its
own render thread, for every GL platform at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
The EGL path from the previous commit is now the only one, so GLSurfaceView
and everything that existed to serve it are gone:
- NativeRenderer.java and NativeGLSurfaceView.java, deleted.
- The javaGL branches in PpssppActivity. GL and Vulkan now share one
lifecycle: a plain SurfaceView, startRenderLoopThread() when the surface
arrives, joinRenderLoopThread() when it goes away.
- The "androidJavaGL" queryConfig, and the AndroidNativeEGL setting and its
developer-tools checkbox.
- displayInit/displayRender, and with them the "Second time, joining the
emuthread and starting it up again" dance - restarting the emu thread from
inside a JNI callback because a lost context was only observable as a
second onSurfaceCreated.
- GraphicsContext::NotifyContextLost, which existed solely because that join
drained the render queue on a thread with no current context. The render
thread now joins the emu thread with the context still current, so the
drain is real work and nothing has to be skipped.
- g_emuThread, now that nothing starts an emu thread outside the render loop.
Three things displayInit/displayRender did that nothing else did, moved
rather than dropped:
- EnterVR and the per-frame StartVRRender/UpdateVRInput/FinishVRRender hooks
now live in RenderLoopThread's GL branch. Java's GL thread could simply
return early and wait for the next vsync when the VR session was inactive;
this loop drives itself, so that case sleeps 16ms instead of spinning.
Not tested - no headset here.
- RECREATE_VIEWS after the emu thread starts, as before. Vulkan never did
this, so it's a candidate for removal when the two paths converge.
- The thin3d error callback, which surfaces GL errors as OSD messages. That
one moved into AndroidEGLGraphicsContext::InitSurface, next to the rest of
the context's own setup, matching WindowsGLContext.
Tested on a nubia Z2464N (Unisoc UMS9632, Mali-G57, Android 15): cold start,
UI, booting a homebrew, backgrounding and resuming both in the menu and
in-game, and a density change, which destroys and recreates the activity the
way a rotation does. Every cycle tears the EGL context down and builds a new
one, and the renderer comes back with FBOs and textures intact. The config
ladder's first choice, RGBA8888 D24S8, is what the Java chooser landed on
here too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
First step towards removing JavaGL. The point isn't EGL itself - it's who
owns the render thread. With GLSurfaceView, Java creates the thread and the
context and calls us back, so a lost context is only observable after the
fact as a second onSurfaceCreated. That's what forces displayInit to tear
down and restart the emu thread from inside a JNI callback, and to call
NotifyContextLost first because the queue is about to be drained on a thread
with no context. None of that is needed when we own EGL: the render thread
joins the emu thread itself, with the context still current, so the drain is
real work rather than something to skip.
AndroidEGLGraphicsContext subclasses OpenGLGraphicsContext (whose own
InitSurface assumes someone else already made a context current) and adds
display/config/surface/context plus a swap function. With it, GL reuses the
Vulkan lifecycle wholesale: queryConfig("androidJavaGL") returns false, so
Java builds a plain SurfaceView and starts the native render loop when a
surface arrives - including all of the surface/pause ordering fixes from the
previous branch. runVulkanRenderLoop/requestExitVulkanRenderLoop are now
runRenderLoop/requestExitRenderLoop and serve both backends; the thread they
spawn branches on NeedsSeparateEmuThread, because the two backends split the
work in opposite directions (Vulkan: this thread is the main loop and the
render manager spawns the render thread; GL: this thread *is* the render
thread and spawns the emu thread).
Deliberately a port, not an improvement:
- GLES client version still follows NativeRenderer - 3 for VR, 2 otherwise.
Not a cap; GLES3 is detected from GL_VERSION at runtime.
- No swap-interval function. GLSurfaceView gave us no way to set one, so
PresentMode::IMMEDIATE has always been ignored here, and honoring it now
would be a behavior change. Worth revisiting once this path is the only one.
The config ladder does take the user's word that Mali 400 and the Xperia Play
are no longer a factor: it asks for RGBA8888/D24S8 and falls back twice,
rather than reproducing GLSurfaceView's ask-for-nothing default that the
comments in PpssppActivity.onCreate exist to explain. ANativeWindow's format
is set from EGL_NATIVE_VISUAL_ID, which is the proper fix for the EGL_BAD_MATCH
those comments describe.
Off by default, behind Developer tools / Graphics / "Use native EGL instead
of GLSurfaceView" (OpenGL backend, Android only). Needs an app restart, since
the context is chosen in NativeApp.init.
Builds for arm64-v8a and on Windows; NOT yet run on a device.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
Dumps before version 4 don't store the game ID, so it's taken from the file
name, which PPSSPP writes as ULUS10064_0001.ppdmp. The ppsspp-framedumps
collection prefixes the GitHub issue number ("11928 ULUS10064_0001 OutRun
water.ppdmp"); skip that, so those dumps get their compat settings too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Partial red, green and blue write masks now always go through the shader with
a framebuffer read. Across the frame dump set this only improves games outside
the old list (Split/Second, Ultimate Ghosts 'n Goblins, older DiRT 2 and OutRun
dumps without a game ID). Partial alpha masks keep the old heuristic: alpha is
the stencil, with its own write mask, and taking the shader path for them made
Pro Yakyu Spirits and Gangs of London worse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--depth-save converts their float depth (z / 65536) to the PSP's 16-bit
values, or writes the unrounded values when the path ends in .f32.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Returning to the CPU flushed all queued drawing, also when a list only reached its stall address.
Wipeout Pure builds its lists as it goes and stalls about a hundred times a frame, so the main thread
spent a third of its time finishing the drawing at those flushes. A stalled list isn't done, so now
only a finished one flushes there, which makes Wipeout Pure 15% faster.
What relied on that flush now flushes itself: memory copies, sets, readbacks and uploads that overlap
the queued drawing, and save states, before memory is saved or loaded (FlushPendingDrawing). Syncs and
the display already did. Queued drawing still reads textures from PSP memory when it runs, so a game
that rewrites a texture between stalls without syncing would see the new one; on hardware the GE has
usually drawn with the old one by then.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Texture pack installs took the destination directory from the archive's
textures.ini, and PKG update installs from the PBOOT's DISC_ID, both
unchecked, so a crafted archive or package could write outside
PSP/TEXTURES or PSP/GAME. Add IsSafePathComponent() to PathUtil and check
both, replacing the WebServer's own upload filename check with it too.
DISC_ID from any PARAM.SFO also names configs, cheats, textures, exdata
and savestates, so ReadSFO now drops one that isn't a safe name and the
game gets a fake ID like homebrew. The same goes for a frame dump's ID.
Combines #22419 and #22420 by Acts1631.
Co-Authored-By: Acts1631 <acts1631kjv@proton.me>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sceKernelStartModule, sceKernelStopModule and the self-stop calls now
check their option first: with SDK 2.08 or later its size must be 0x14,
and attribute bits outside 0x00F06000 are an error.
The option's attribute is added to the module's thread attribute rather
than ignored (or, when stopping, replacing it), and the default attribute
is 0 rather than the module's own attribute. A nonzero mpidstack puts the
stack in that partition, which has to be user-accessible for a user
module and kernel-only for a kernel module.
A failed thread creation is now returned instead of asserting.
Hardware test: modules/startoptions.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
modulemgr only defaults a module_start/module_stop thread's stack to 0x40000
for user modules. Kernel modules get 0x1000. We gave every
module 0x40000, which mostly cost kernel memory when booting the VSH.
User modules, and so games, are unaffected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Matching packet handlers added untrusted payload lengths to fixed header
sizes before comparing them with the datagram length. Large values could
overflow and permit out-of-bounds reads. Compare against the remaining
packet size instead, and reject negative ACCEPT sibling counts.
Lets a dump replay stopped with --replay-end be compared against the PSP on
an offscreen buffer. The framebuffer commands after the draw limit are
dropped too, so it shows the buffer the last drawn primitive went to.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--depth-save writes the depth buffer next to the screenshot (u32 width, u32
height, then u16 depths), and --replay-end stops a frame dump replay after
that many primitives. Both match pspautotests' ppdmp-playback, so a dump's
depth can be compared with the PSP's at any point in the frame.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
hleDelayResult and Sas's delay put the wait on whatever thread was
current, which in an interrupt handler is the idle thread it runs on.
With both idle threads waiting there's nothing left to schedule, the
same crash as sceDisplaySetMode in Gods Eater Burst's vblank handler.
Where pspautotests intr/delays shows what hardware returns there,
match it: sceIoOpen and __sceSasCore fail with ILLEGAL_CONTEXT,
sceMpegCreate with 80618008, and sceAtracReinit can deinit but not set
up the codec (807f00fd).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
wsdbg --launch learns the port from our output, which without --log was
nothing. Also set the port before reporting the server as running, so
WebServerWaitForStartup() callers can read it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Gods Eater Burst calls sceDisplaySetMode from its vblank handler. Its
implicit vblank wait went to the idle thread the handler runs on. Since
vblank waiters are released ~48us after the vblank (02b2b7b699), the next
frame's handler runs, on the other idle thread, before the first one is
released, so both idle threads end up waiting and the scheduler asserts
with an empty ready queue on leaving the interrupt.
On hardware sceDisplaySetMode returns 0 without waiting there, and with
interrupts or dispatch disabled (pspautotests intr/waits).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Achievement badges and game icons don't change, so re-downloading them
daily was wasted. InsertIcon now takes a max age, and each entry saves
its expiry time. Leaderboard avatars get a day, everything else 30 days.
Bumps the cache version.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With RAIntegration, Initialize leaves login to the DLL load, but WinMain
only calls InitializeRAIntegration once and Shutdown unloads the DLL, so
turning achievements off and on left you logged out until restart.
Remember the window and load the DLL again on its thread.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The deprecated unsized rc_client_deserialize_progress parses with no
bound, so a truncated or corrupt state could make it read past the
buffer. data_size is known, so pass it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rc_client answers a second login with RC_INVALID_STATE, synchronously,
and login_password_callback then cleared g_isLoggingIn while the token
login was still running. IsBlockingExecution stopped blocking, and a game
started then ran without achievements.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On invalid credentials the token was kept, and Idle() retried it every
10 seconds for the rest of the session, silently after the first attempt.
Clear it, as was already done for an expired token, so the settings show
the login form. On access denied, stop retrying.
Show a rejection even when it comes from a background retry, since it
won't repeat, and have LoginProblems return the server's message.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rc_client_get_rich_presence_message returns the length it needed rather
than what it wrote, so a message of 512 characters or more made the pause
screen read past its buffer.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rc_client keeps raw pointers to itself in the callback data of award,
leaderboard submit and ping requests, so destroying it with one in flight
(disabling achievements mid-game) had the late callback write to freed
memory. Count requests per client, retire a busy client instead of
destroying it, and destroy it when its last request completes.
Request timeouts in naett are per operation and the plain HTTP client has
none for the body, so a retired client is also destroyed after 60 seconds
regardless. Its late callbacks are then dropped, leaking their data.
Clients are tracked by ID so a new client at a reused address can't get an
old one's callbacks. At app exit, pending requests are cancelled without
callbacks, so everything is destroyed immediately.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
naett reports a connection failure as -1, which rcheevos reads as
RC_API_SERVER_RESPONSE_CLIENT_ERROR, a non-retryable client error. An
unlock or leaderboard submission during a network dropout was then
treated as a server rejection and dropped, on every platform using naett.
Map any non-HTTP result to RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rc_client doesn't call back for a game load aborted by unload, or for
anything pending when the client is destroyed. g_isIdentifying or
g_isLoggingIn then stayed set, and IsBlockingExecution kept every later
boot on a black screen until restart. Quitting during identification, or
disabling achievements during a token login, was enough.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
UWP compiled naett with the WinHTTP backend but never reported
SYSPROP_SUPPORTS_HTTPS, so achievement traffic and badge icons went over
plain http. When the media server redirected to https, icons failed.
A failed icon download also left the URL marked pending, so it was never
retried for the rest of the session. Mark it failed instead, and let
IconCache retry it after 30 seconds.
The set-completed badge also skipped RemoveHttpsIfNeeded.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The release was counted down on the WebSocket thread, one step per poll
of host time however many vblanks had passed, so how long a scripted
press lasted depended on how fast the emulator ran, and scripted runs
went different ways. sceCtrl now releases it after that many vblank
samples, on the emulator thread; the debugger only reports when it's done.
Also: wsdbg's :screenshot works in headless with Vulkan.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On hardware the last part of a savedata shutdown runs at priority 0x20,
whatever the dialog's own thread priorities, so a caller at 0x20 gets the
CPU back first and sees SHUTDOWN, and one at 0x21 or worse only sees NONE
(pspautotests utility/savedata/shutdownstatus). We ended it at the access
thread's priority, so Freak Out, which calls ShutdownStart from 0x20 and
waits for SHUTDOWN, sat at 'Please press START' forever. NFL Street 3,
which calls it from 111 and then InitStart straight away, still gets NONE.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Taking the highest unused slot instead could steal one that a state event
restores later, as VBlankWake did to MicBlockingResume, which then had
nowhere to go. Also name the event in the assert.
AGENTS.md: When a savestate fails to load, suspect the branch first.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Recorded on hardware (pspautotests threads/tls/timeout), a Tlspl
allocation follows the same timeout rule as the other waits, including
failing at once for 0 and 1us without writing the timeout back, which the
shared rule it moved to in the last commits didn't give it yet. Before
that it waited the raw timeout, ~30us short.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Priority-ordered waiting lists were sorted with a comparator wrapper per
object (msgpipe, fpl, vpl), or searched with a copy of the same function
(mutex, mbx). HLEKernel::SortWaitingThreadsByPriority() and
FindBestPriorityWaiter() now do both for any waiting list, of thread ids
or of structs with a threadID.
HLEKernel::ClearWaitingThreads() replaces the identical cancel/delete
loops in semaphores, event flags, fpl and vpl.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
A state with more event types than are registered now was refused, so no
event could ever be removed or merged. Loading now keeps the extra slots:
modules that still know an old event restore it to a handler, and the rest
stay placeholders that do nothing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An interrupt with no handler to run, a vblank with none registered for
example, switched the running thread off to idle and left it there until
some later event rescheduled: ~775us of every frame in a game that spins
without a vblank handler. It now reschedules at once. Taking an interrupt
also clears the ll bit directly, which that switch had been doing.
Interrupt handlers can now carry a cost before they run and after the
last queued one returns. Alarms use it: on hardware a thread that keeps
running loses ~70us to an alarm handler, and a thread the handler wakes
runs ~50us after it (pspautotests threads/scheduling/alarmcosts), so
17us in and 40us out. sceKernelSetAlarm's 40us is split evenly around the
deadline, keeping the handler ~1040us after a 1000us alarm.
A handler's return value re-arms its alarm counting from the previous
deadline, so a repeating alarm doesn't drift by those costs, unless
that's already past, as after interrupts were suspended for a while.
The vblank's own cost (~62us of CPU on hardware) isn't charged yet: with
it, a waiter ~90us after the vblank still reads hcount 1 on hardware, but
line 2 here. Hardware evidently raises the interrupt ~40us before the
line count wraps. That's noted where the waiters are released.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On hardware a thread waiting for vblank returns ~53us after it, where we
had it back in ~5us, and the first of four waiters runs after ~85us: each
waiter beyond the first adds ~9us (pspautotests threads/scheduling/
vblankwake). The waiters are now released by a separate event 48us + 9us
per extra waiter after the vblank. Which vblank a wait is for is still
decided at the vblank, so a thread that starts waiting in between still
waits a whole frame (sceDisplay section version 8).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Timed on a PSP, each way of handing the CPU to another thread (the call
and the switch together):
hardware before now
rotate to an equal thread 7 14 7
signal, better thread runs 10 17 10
it waits again, back to caller 10 19 12
wakeup, better thread runs 8 13 6
it sleeps again, back to caller 7 12 6
start a better thread, entry 30 28 30
thread ends, back to its waiter 21 13 20
notify, better thread's callback 14 13 14
A switch between two threads now costs 1150 cycles instead of 2700.
Starting a better thread costs 2000 cycles more, ending a thread 3300,
and setting up a callback 1800.
Also splits a wait timeout's ~30us into the deadline being taken 12us
into the call and the timeout going off 18us after it. That only changes
the time left written back, which threads/semaphores/wait and
threads/fpl/cancel pin between them. intr/vblank is re-recorded so it
no longer depends on the phase of the frame.
threads/callbacks/combos now passes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On hardware, notifying a callback of a thread in a CB wait takes it out of
the wait at once, even though the callback only runs when the thread would
get the CPU. A semaphore signalled in between doesn't end the wait: the
callback runs first, then the wait resumes and takes it (pspautotests
threads/callbacks/combos). We left the thread on the wait list until the
callback started, so the signal ended the wait and the callback didn't
run.
The notify now pauses the wait, as starting a callback used to. If the
callbacks are canceled before the thread's turn comes, the wait just
resumes (Thread savestate section version 7).
threads/callbacks/combos goes in the to-do list: a callback returning to
the thread that notified it still takes ~13us where hardware takes ~9,
part of the context switch cost.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>