Commit Graph
100 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5 8632b726f4 headless: keep --cpu from switching on the vertex decoder JIT
g_Config.iCpuCore does double duty: it picks the MIPS core, and it gates the
vertex decoder JIT in the DrawEngineCommon constructor. Headless forces it to
INTERPRETER to keep that decoder off, but it did so before ApplyToConfig(), so
--cpu=jit and --cpu=jit-ir overwrote it and enabled a GPU path the tests aren't
recorded against - 16 of them failed on x86-64, gpu/vertices/morph among them.

Force it after ApplyToConfig() instead. The core the tests actually run on
comes from CoreParameter, straight off the command line, so the backends are
still tested; only the GPU side is pinned.

Also limit the frametests to x86-64. The reference images don't match the arm64
software renderer - 23 of 30 dumps differ, reproducible on any arm64 host. That
predates the new runner, which just gave it somewhere to show up.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-19 13:02:36 -06:00
Henrik RydgårdandClaude Opus 5 bb556bf481 CI: run pspautotests on all four CPU backends
The headless tests only ever exercised the JIT, since that's what headless
defaults to. Run all four on the Linux runners, and add an arm64 Linux lane
so the arm64 JIT is covered too - nothing else in the matrix tested it.

test.py scales the wall clock to the backend instead of raising it for
everyone: the interpreter needs 20s for gpu/rendertarget/copy, which does
over a million guest-side vsprintf calls, while a hang under the JIT is
still caught in five seconds.

The frametest report artifact needs a per-OS name now that two Linux legs
upload one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-19 12:20:48 -06:00
Henrik Rydgård a90087012d Merge pull request #22311 from hrydgard/mips-interpreter-fixes
Claude code review: MIPS interpreter
2026-09-19 10:55:28 -06:00
Henrik RydgårdandClaude Opus 5 b03bbdbca3 MIPSTracer: don't index trace_info with stale block indices
flush_to_file() ends with clear(), but tracing stays on and the blocks
already compiled keep the index baked into their LogIRBlock instruction.
A second flush then indexed an emptied trace_info out of bounds.

Skip indices that no longer refer to anything and say so in the log, and
give the LogIRBlock placeholder an explicit invalid index so a block that
prepare_block failed to record doesn't dump block 0 instead.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-19 10:02:54 -06:00
Henrik RydgårdandClaude Opus 5 675ad0acd6 Interpreter: assorted fixes found while reviewing Core/MIPS
WriteMMIO_U32 was missing the return after the kernel-mode check, so a
user-mode write raised the exception and then went through anyway. The
other five MMIO accessors already return here; this one lost it when the
GPIO/syscon branches were added.

Int_Vrot scanned all four entries of dregs, but GetVectorRegs only fills
the first n - so a vrot with vs == 0 (S000) matched a lane that isn't
there and took the cosine from it. The IR backend gets this right via
IsOverlapSafe, so the two disagreed.

The breakpoint checks in MIPSInterpret and RunUntilDowncountZeroWithChecks
read instr->flags without checking for null, which MIPSGetInstruction
returns for the eight primary opcodes (and many subops) that don't decode.
With a memcheck or register breakpoint active, landing on one of those
crashed instead of raising ILLEGAL.

Also: GetVectorOverlap decoded the second vector with size1 (no callers
today), and RegisterFunction left most of its AnalyzedFunction
uninitialized, including the size that ends up in knownfuncs.ini.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-19 10:02:46 -06:00
Henrik Rydgård 99fecc1874 Merge pull request #22310 from hrydgard/kprintf
KDebugForKernel: implement Kprintf
2026-09-18 16:59:23 -06:00
Henrik Rydgård 8f3da1a84a Merge pull request #22233 from hrydgard/combo-suppress-singles
Don't fire single-button mappings while a combo using them is held
2026-09-18 16:58:46 -06:00
Henrik Rydgård 5c9eda7cd8 KDebugForKernel: implement Kprintf
Outrun 2006's USB kernel module calls it on a failed sceUsbbdRegister,
so booting the game printed "Unimplemented function Kprintf". It's the
kernel's debug printf - on hardware it goes to whatever
sceKernelRegisterKprintfHandler() installed, which on a retail PSP is
the serial port, so this is just debug output the game shipped with.
We format it and log it, which is the useful thing to do with it.

The vararg walker that sysclib's sprintf/snprintf already had is now
HLEFormatPrintf() in HLE.cpp, so there's one of these rather than a
second copy. It takes the index of the first vararg (counting a0 as 0)
instead of an offset from a2, which is the same mapping written in
absolute terms - sprintf passes 2 and snprintf 3, where they passed
0 and 1 before.

Kprintf replaces the existing nullptr entry in the KDebugForKernel
table, so no indices move and savestates are unaffected.
2026-09-18 16:26:41 -06:00
Henrik Rydgård 2ffe132516 Merge pull request #22309 from hrydgard/lle-mpeg-mp4-default
New default: sceMpeg and sceMp4 now run as LLE (DisableHLE)
2026-09-18 15:43:22 -06:00
Henrik RydgårdandClaude Opus 5 19bac4894c Let sceMpeg LLE override the ForceHLEPsmf compat flag
Our psmf and psmfPlayer HLE plays video by calling our sceMpeg HLE, so it has
nothing to talk to when the real mpeg.prx is running. The flag now gives way
when sceMpeg is LLE.

Moved below the force-enable and unavailable masks so it tests what sceMpeg
actually ended up as rather than what was asked for.

Also remove a bad assert.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 15:15:45 -06:00
Henrik Rydgård 094d28d451 Misc portrait layout fixes, minor build.gradle.kts change 2026-09-18 14:08:08 -06:00
Henrik RydgårdandClaude Opus 5 0990890640 Translate the sceMp4 firmware message
Follows each language file's own word for firmware rather than imposing one, so
this reads the way the neighbouring strings in [System] already do: fastvare in
Norwegian, systemprogramvara in Swedish, laiteohjelmisto in Finnish, proshivka
in Russian and Ukrainian, and the English word where that is what the language
has settled on.

Thirteen files keep the English string as a placeholder. Those are the ones with
no translation for the firmware strings already in that section either, so there
was no house style to follow and guessing would leave nobody able to check it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 13:50:25 -06:00
Henrik RydgårdandClaude Opus 5 9b78155ebd Settle the wording of the two video firmware messages
The sceMpeg one goes away: our HLE handles almost everything, so ending up on it
isn't worth interrupting the player over. It stays in the log, where it explains
why a video might not look the way it does with the real module.

The sceMp4 one is the one that matters, since there is no working HLE behind it,
and it now says "installed firmware" rather than "firmware dump" - PPSSPP
installs firmware much as a PSP does these days, so that is the wrong word.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 13:50:25 -06:00
Henrik RydgårdandClaude Opus 5 f6655d8b62 add-string: read the invocation instead of splitting it
The command took $1/$2/$3 off the front of whatever was typed, so it only got
the right section and key when called in exactly the documented shape. Called
with a sentence - which is the natural way to ask for this - it silently
produced three arbitrary words, and a paragraph warning about that is a poor
substitute for not doing it. It now gets $ARGUMENTS whole and works the three
values out, which is the part that needed a model anyway.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 13:50:25 -06:00
Henrik RydgårdandClaude Opus 5 32017202bf Use the disc's own mpeg.prx, and say when sceMp4 won't work
A game that ships MPEG.PRX doesn't need a firmware installed to run the real
module - Death Jr. loads PSP_GAME/USRDIR/MODULES/MPEG.PRX itself - but the flag
came off anyway, because the decision has to be made before the game's imports
resolve and nothing had looked on the disc yet. So look: a bounded walk for a
file of that name, only when flash0 comes up empty, so the usual case pays
nothing.

The name is the easy part - about a quarter of discs ship one and it is called
mpeg.prx in every case seen - but the directory is not. MODULE and MODULES are
the common ones, with KMODULE, PRX, DATA/MODULE, and more at five levels deep,
hence the generous depth limit. Matching on the name rather than reading each
PRX to see what it exports means guessing wrong only costs us the real module.

sceMp4 is the other half of this. Our HLE of it is nearly all stubs, so dropping
the flag for want of firmware doesn't rescue anything - those libraries only
exist in firmware 6.00 and later, and without them MP4 playback simply isn't
available. Since almost nothing uses sceMp4, warning about that every boot would
be noise, so NotifyLoadStatusMp4 says it instead, which only something actually
asking for MP4 reaches.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 13:49:10 -06:00
Henrik RydgårdandClaude Opus 5 c39db565da Run sceMpeg and sceMp4 for real by default
Both now graduate to AlwaysDisableHLEFlags, so a firmware dump gets the real
mpeg.prx, libmp4.prx and mp4msv.prx without anyone having to find the setting.
The checkbox moves from "Disable HLE" to "Force-enable HLE" on its own, so a
game that regresses still has a way back.

Neither can be counted on being there, so HLECheckModuleAvailability drops the
flag when the module is missing and the HLE serves as before - the same thing
sceFont does when flash0:/font is empty. Its sceMp4 check asked the setting,
which is no longer where the answer is now that the default is on; it asks
AlwaysDisableHLEFlags instead, and sceMpeg gets a check of its own.

Both are quiet about it now. Missing firmware used to mean a request we couldn't
honour, which was worth a warning on screen; now it just means the user has no
dump, which is the ordinary way to run PPSSPP.

The cost is that a disc carrying its own mpeg.prx also falls back when there's
no firmware, though it would have run fine. The choice has to be made before the
game's imports are resolved and there's no telling then whether a module will
appear later, and getting it wrong leaves the game importing from a module that
never loads.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 12:55:08 -06:00
Henrik Rydgård 95e12a05cf Merge pull request #22307 from hrydgard/csc-perf
sceMpeg LLE: Improve color space conversion perf by using sws_scale
2026-09-18 12:54:20 -06:00
Henrik Rydgård f4e7058e54 Merge pull request #22308 from hrydgard/android-script-cleanup
Android script cleanup
2026-09-18 12:22:26 -06:00
Henrik RydgårdandClaude Opus 5 f3b578bd53 Build TestMpegCsc on Android too, and say that it needs doing
The legacy Android build has a unit test executable of its own, so a new file in
unittest/ goes in three build files rather than the two the docs named. Missing
the Android one builds fine everywhere it is convenient to try and fails only on
Android CI, which is what happened here - so both AGENTS.md and building.md now
say three, and which one is easy to forget.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 12:13:17 -06:00
Henrik RydgårdandClaude Opus 5 5c5c7dec86 Include what these files use
UnitTest.h's EXPECT_ macros all call printf and EXPECT_EQ_MEM calls memcmp, but
it included neither <cstdio> nor <cstring> - it has been relying on whatever the
including file happened to pull in first, and TestMpegCsc was the first not to.
The same shape in sceMpegbase.cpp and sceVideocodec.cpp, which use std::min,
std::move and memcpy without saying where they come from.

Also drop an abs() from TestMpegCsc rather than include <cstdlib> for one
subtraction.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 11:59:48 -06:00
Henrik Rydgård d6eb19e52a Update Android Gradle Plugin to 9.4.1 2026-09-18 11:53:08 -06:00
Henrik RydgårdandClaude Opus 5 da882910e3 Android: remove dead Ant/Eclipse build scripts
The Ant build hasn't been runnable since the SDK dropped tools/ant/, so build.xml,
custom_rules.xml, ab-ant.sh, ant-build.bat, project.properties and
proguard-project.txt all go. Plus some orphans: buildassets.sh (nothing called it),
build.sh (still referred to "phoenix"), symx86.cmd (x86 isn't in APP_ABI) and
README.TXT (Eclipse import instructions).

ab.sh and ab.cmd no longer copy assets into android/assets - only the Ant/Eclipse
packaging ever read that directory. Gradle packages the repo-root assets/ directly
and ndk-build doesn't look at assets at all, so the stray copy only made it unclear
where the APK's assets come from. Noted how that actually works in docs/building.md.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 11:51:54 -06:00
Henrik RydgårdandClaude Opus 5 8fa0826bdb sceMpegbase: init and shut down from the kernel, like everything else
__MpegBaseInit was called by __MpegInit rather than by __KernelInit, and
__MpegBaseShutdown had just been added the same way. Every other module is
started and stopped directly by the kernel - __MpegBaseDoState already was -
so do the same here and let sceMpeg.cpp mind only its own state.

The order is unchanged: __MpegBaseInit ran first inside __MpegInit and now sits
just before it, __MpegBaseShutdown ran last and now sits just after.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 11:38:49 -06:00
Henrik RydgårdandClaude Opus 5 dc743d4fce sceMpegbase: free the conversion's memory on shutdown
__MpegBaseShutdown, hanging off __MpegShutdown the way __MpegBaseInit hangs off
__MpegInit, so the de-tiling scratch and the swscale context go back when the
game stops rather than only when the next one starts. Between them they are a
few hundred kilobytes that a game which played one video early on has no further
use for.

Also name the swscale flags rather than passing SWS_POINT inline, and say next
to it what the choice actually decides - with equal sizes in and out it is only
how chroma gets to full resolution, and SWS_BILINEAR (what the HLE uses) is a
one-line swap. Worth a real option one day; not adding one now.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 11:38:49 -06:00
Henrik RydgårdandClaude Opus 5 4cb01fea67 sceMpegbase: convert with swscale, keeping the scalar path as the fallback
The planes the de-tiling produces are already the YUV420P swscale wants, and our
sceMpeg HLE converts the same frames the same way, so the pixel formats and the
studio-range setup come straight from MediaEngine::getSwsFormat. It is 3-4x
quicker than going a pixel at a time: 0.37-0.44ms a frame becomes 0.09-0.12ms,
which is the whole reason sceMpegBaseCscAvc was at the top of a profile.

Chroma is upsampled with SWS_POINT rather than the HLE's SWS_BILINEAR, since
replicating is what the scalar path does and, being a fixed-function block,
almost certainly what the hardware does.

It is not bit-identical - swscale rounds its own way. TestMpegCsc measures the
gap per channel rather than per byte, so the number means something for a packed
16-bit pixel: worst 1 step of 31 for 5650 and 5551, 2 of 15 for 4444, 3 of 255
for 8888, with means around a fifth of a step. The scalar path stays as what the
longhand reference is checked against, and takes anything swscale won't - an odd
range origin, or a build without ffmpeg.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 11:38:49 -06:00
Henrik RydgårdandClaude Opus 5 02906f0510 sceMpegbase: write alpha as zero, and stop rebuilding the planes every frame
The colour conversion was writing alpha fully set - 0xFF000000, or the top bit
for 5551 - where the hardware writes zero. Our sceMpeg HLE already masks it off
and names Sword Art Online as a game that depends on it: it doesn't clear the
alpha in the buffer it hands over, and expects the video not to set it. The two
paths now agree.

The de-tiling ahead of it becomes UntileYCbCr, taking the eight buffers already
resolved, so it can be measured and compared against the original longhand
version in TestMpegCsc. Its planes move to scratch that persists between calls -
a movie converts one frame per displayed frame, and this was allocating and
clearing about 200KB every time - and the per-pixel bounds checks in the chroma
loop, which only depend on the group of eight, are hoisted out of it.

That last part is worth 2529 -> 3201 MPix/s, but the point of measuring was to
find out whether it mattered, and it doesn't much: de-tiling is 0.04ms of a
frame against the conversion's 0.4ms. The conversion is where the time is.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 11:38:49 -06:00
Henrik RydgårdandClaude Opus 5 cf1d513c9e Don't default to building x64 on a machine that isn't
The MSBuild examples passed /p:Platform=x64 and the run lines pointed at
Windows/x64/..., so following them on an ARM64 machine produced an x64 build -
which then runs anyway under emulation, so nothing looks wrong. It is slower
than the native build, it isn't the code ARM users get, and a benchmark taken
from it measures the emulator: the colour conversion benchmark this was noticed
on reads 200 MPix/s emulated against 300 native.

The examples now say <platform> rather than either value, so there is no default
to follow and the machine has to be looked up. Also note that
$PROCESSOR_ARCHITECTURE describes the shell, not the host, and says AMD64 from
an emulated shell.

test.py searched only Windows\x64 for the headless binary, so on Windows-on-ARM
it would silently test an emulated build; it now looks for the native one first.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 11:38:49 -06:00
Henrik RydgårdandClaude Opus 5 a6996b2c3f sceMpegbase: pull the colour conversion out, and measure it
sceMpegBaseCscAvc is the top of a profile during video playback, so the loop
that does the work becomes MpegCscRange - a pure function with the HLE plumbing
left behind - and TestMpegCsc measures and checks it.

The measuring half reports megapixels per second for a 480x272 frame in each of
the four pixel formats. The checking half compares against the conversion
written out longhand, over whole frames and over partial ranges with odd offsets
and sizes, plus one-pixel, one-row and one-column ranges and one that reaches
the far edge of the frame. Those are the cases an optimized version gets wrong:
chroma is half resolution, so an odd left edge starts mid-sample, and anything
handling two pixels at a time has to deal with the leftover. The destination is
padded and prefilled, so writing outside the range fails too.

This is only the move - the loop is the same one, so the numbers it gives are
the baseline to improve on. On a Snapdragon X Elite it runs at about 300 MPix/s,
0.44ms for a frame.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 11:38:49 -06:00
Henrik Rydgård 21c7312486 Merge pull request #22306 from hrydgard/assorted-fixes
Various fixes to sceMpeg LLE and sceMp3 LLE
2026-09-18 10:11:22 -06:00
Henrik RydgårdandClaude Opus 5 01be454b7a sceMp3: keep accepting sample rates a PSP would refuse, on purpose
libmp3.prx only accepts a rate other than 44.1kHz from a game built with SDK
3.09.05 or later, and audio/mp3/init has the hardware's answers for the rest.
PPSSPP has nonetheless accepted them from every game that declares an SDK
version, because the threshold was written as decimal 3090500 rather than
0x03090500 - an accident, but one people have come to rely on. Beats and games
like it build levels out of MP3s the user supplies, and refusing an ordinary
48kHz file looks like a bug to whoever supplied it.

So the hardware answer now goes only to something that declares no SDK version
at all, which in practice means the test, and the comment says that is a choice
rather than an oversight. Being strict again is a one-line change, with the two
lines it would cost named next to it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 09:45:11 -06:00
Henrik RydgårdandClaude Opus 5 e6fa47291d Log: give the printf output the same format as the others
Headless is the only thing that uses it, and it had its own shorter format with
no thread, file or line, so a pattern that matched a log from the app quietly
matched nothing in one from headless. Costs a wrong conclusion the first time
you do it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 09:45:11 -06:00
Henrik RydgårdandClaude Opus 5 7ed0598433 sceAudiocodec: report the decoded size in bytes, not samples
The field at 0x24 is a byte count, like srcBytesRead next to it, and
sceAudiocodecGetOutputBytes describes the same quantity the same way (0x1200
for MPEG1 MP3). We were putting the sample count there, a quarter of the value,
and libmp3.prx takes it as the length of the PCM to pass on. Renamed to
dstBytesWritten so it reads like what it is.

Nothing on our side consumed the field, so this only changes what the firmware
modules see. mpeg.prx ignores it, which is why Atrac3+ playback was unaffected
either way, but libatrac3plus.prx does read it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 09:44:39 -06:00
Henrik RydgårdandClaude Opus 5 08573668c5 sceAudiocodec: fill in the MP3 version in GetInfo, log what GetInfo read out
sceAudiocodecInit puts 9999 in the version field to mean "not known yet", and
filling it in is what this call is for - libmp3.prx reads it straight back out.
We wrote every other MP3 field and left that one alone, so the real libmp3.prx
got as far as GetInfo and then stopped without ever asking for a decode.

While here, read the fields off the frame rather than claiming 128kbps 44.1kHz
stereo unconditionally, which is what the hardware does with them. They are the
raw MPEG header fields apart from the version index, which has its own
numbering. The old fixed values stay as the fallback for when there is no
readable frame to look at.

Also carries a CheckNeedMem log tweak that was already in the tree.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 09:43:39 -06:00
Henrik RydgårdandClaude Opus 5 b77e7c4e66 sceVideocodec: implement CopyYCbCr
This is what sceMpegAvcCopyYCbCr is built on, and a game that wants the raw
YCbCr rather than letting sceMpegbase convert to RGB uses it and nothing else.

mpeg.prx builds the descriptor on its own stack and avcodec.prx reads it back at
0x800015c4. Dimensions in pixels at 0x00/0x04, the eight frame buffers from 0x0c
but ordered 0,2,4,6 then 1,3,5,7, and from 0x2c the destination Y with Cb and Cr
following it contiguously - ordinary planar YUV420. Checked against what the
game passes: the eight addresses are exactly the buffers we handed out, and the
three destinations are spaced width*height and width*height/4 apart.

Un-tiling is the same operation the colour conversion already does, so that
moves out of sceMpegbase.cpp as ReadTiledYCbCr rather than being written twice.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-18 09:43:15 -06:00
Henrik Rydgård 3b2f0f0c1f Merge pull request #22304 from saboten731/fix/hleinit-filesystem-split-pr
HLE: split filesystem-dependent availability checks from initialization
2026-09-17 22:23:57 -06:00
Henrik Rydgård 2385698f8e Merge pull request #22303 from hrydgard/headless-timeout-split
Headless timeout: Split into game-time timeout and wall-time timeout
2026-09-17 17:28:45 -06:00
Henrik RydgårdandClaude Opus 5 27f5a9758c headless: accumulate emulated time instead of trusting the clock
Loading a savestate sets the emulated clock to whatever it read when the state
was written, which can be a long way either side of where this run is - so a
deadline of "start plus N" was already in the past the moment --state landed,
and --timeout-emulated fired immediately. Accumulate the per-iteration steps and
ignore ones too large to be elapsed time.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 16:01:47 -06:00
Henrik RydgårdandClaude Opus 5 e8fa4e3f56 headless: split --timeout into --timeout-wall and --timeout-emulated
--timeout was wall-clock seconds, which is what CI wants but not what you want
when the question is whether the game has had long enough to get somewhere: a
heavy scene runs many times slower than real time and a near-idle one much
faster, so the same budget means very different amounts of game time. Booting a
firmware VSH is a good example - 10 emulated seconds is about 25 real ones on
6.61 and about 7 on 2.00, and judging those two by the same wall-clock number
makes a working shell look stuck.

Both limits can be set at once and whichever is reached first ends the run,
which also says which one it was. --timeout still works as the old name for
--timeout-wall. The IsDebuggerPresent() exemption stays on the wall-clock check
only; the emulated one doesn't need it, since sitting at a native breakpoint
burns no emulated time.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 16:01:47 -06:00
Henrik Rydgård bec55c61d1 Merge pull request #22237 from hrydgard/naett-cancel
http: Make Cancel() actually stop an HTTPS transfer
2026-09-17 16:00:12 -06:00
Henrik Rydgård ce42033686 Merge pull request #22302 from hrydgard/real-mpeg-prx-fixes
Even more fixes for real sceMpeg
2026-09-17 15:57:47 -06:00
Henrik RydgårdandClaude Opus 5 21928ed43f sceVideocodec: let stopping and deleting the decoder take time
Both are ME round-trips that take real time on hardware, and returning from them
immediately matters beyond speed. Jak and Daxter deletes its video_sound_thread
straight after sceVideocodecDelete without waiting for it to exit. With no time
passing in the delete, the game's audio thread never gets to run once more and
deliver the wake that lets that thread notice the shutdown and exit, so
sceKernelDeleteThread fails with NOT_DORMANT and the thread stays alive. It is
then released from the event flag the game has just deleted, resumes on a
context that has already been freed - every id and pointer in it zero - and
copies from a null pointer.

2ms, chosen to sit above the 1.45ms an audio mix block takes. 100us was measured
to be too short, so the fix is the time passing rather than just the reschedule.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 15:18:06 -06:00
Henrik RydgårdandClaude Opus 5 dba91883ec sceDisplay: don't carry a host timestamp across boots
The fast-forward flip limiter in __DisplayFlip kept its last-flip time in a
static local, so it survived a boot and the first flip of a new game was
compared against a timestamp from whatever ran before it. Move it up with the
other frame timing globals, which __DisplayInit already resets.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 13:51:45 -06:00
Henrik RydgårdandClaude Opus 5 0c926a337b headless: end and restart the host frame each emulated frame
The app opens a host frame, runs one frame and closes it again, but headless
opened a single one around the whole run. All of the GPU's per-frame work hangs
off BeginHostFrame - the texture cache's StartFrame and the framebuffer
manager's BeginFrame, which is what decimates FBOs - so none of it ran here at
all, and a long run decayed nothing.

That isn't only a rendering difference: the framebuffer state it maintains
decides whether gpu->PerformMemoryCopy claims a copy, and a claimed copy skips
the write to emulated memory entirely. So a stale cache could change what a
game sees in RAM, not just on screen.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 13:29:28 -06:00
Henrik RydgårdandClaude Opus 5 5cfd859eae headless: start from the real config defaults
Headless never loads a config file, so every setting it doesn't assign kept the
zero-initialized value instead of the default the ConfigSetting table declares.
140 settings were affected, and among them were ones that change how games run,
not just how they look: bFastMemory and bFuncReplacements are both "true" by
default and were false here, so headless was the only build compiling memory
accesses the slow way and running games without function replacements.

Call RestoreDefaults before the block that forces the values the tests want, so
those still win and the rest now match every other build.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 13:18:00 -06:00
Henrik RydgårdandClaude Opus 5 0207c6d04a Clear the codec context maps per boot, and replace functions in headless too
sceMpeg and sceAudiocodec only cleared their context maps on shutdown, while
sceMpegbase and sceVideocodec clear theirs on init. Both maps are keyed on an
address the game chooses, and getMpegCtx reads its key straight out of game
memory, so anything left behind can be handed to the next game we run in the
same session. Clear them on init as well. sceVideocodec's init cleared its map
without deleting the decoders in it; use ClearContexts for that.

Headless never loads a config file, so every g_Config field it doesn't set
keeps the zero-initialized value rather than the ConfigSetting default.
bFuncReplacements is one of those, so headless was the only build running games
without function replacements - which is why a crash in Jak and Daxter's
memcpy_jak wouldn't reproduce there.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 12:51:52 -06:00
Henrik Rydgård bb906f33aa Merge pull request #22299 from hrydgard/real-mpeg-prx-fixes
More fixes for real sceMpeg on top of sceVideocodec (DisableHLE)
2026-09-17 12:09:26 -06:00
Henrik Rydgård 4f54119d4f Merge pull request #22300 from hrydgard/headless-forward-stdio
Headless: forward the program's stdout/stderr to the host by default
2026-09-17 12:05:21 -06:00
Henrik RydgårdandClaude Opus 5 c596bdf9a4 sceAudiocodec/sceVideocodec: fill an AAC gap and name two ME functions
sceAudiocodecCheckNeedMem set neededMem for every codec except AAC, where it
was left at whatever was in the context. Set it to 0x18f20 like the others; our
faked ME memory meant this never blocked, but a game that validates it could.

Also name the two hash-named sceVideocodec exports from their firmware
behaviour: 0x893B32B1 configures the codec during sceMpegCreate in mode 1
(SetMode), and 0xD95C24D5 copies a decoded YCbCr frame between buffers via the
ME (CopyYCbCr), the videocodec-level counterpart of sceMpegBaseYCrCbCopy. Both
are still stubs - only mpeg.prx calls them, on paths nothing we run reaches -
but they are now documented for whoever implements them.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 11:01:09 -06:00
Henrik RydgårdandClaude Opus 5 ba7bf86e73 sceAudiocodec: don't return a zero Atrac3+ frame size for odd bitrates
CalculateInputBytesAndChannelsAt3Plus only set the frame size for the four
bitrates it had a table entry for and left it at 0 otherwise, which fails the
decode - the same shape of bug the AAC path just had. formatByte2 * 8 + 8 is
the size for all four known bitrates, so use it for the rest too; the firmware
never returns 0 here. The common PSMF path is unaffected: it takes the size
from the frame's own header before reaching this.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 10:45:16 -06:00
Henrik RydgårdandClaude Opus 5 b561de3ff1 sceAudiocodec: size the AAC input frame like the firmware
The AAC decode path read the frame size from srcBytesRead, which is an output
field the decoder writes - it's 0 on the first call, so the decoder was handed
0 readable bytes and audio never started. avcodec.prx's decodeUtility sizes the
AAC input from the byte at 0x2c instead: 0x609 when nonzero, 0x600 when zero.

Investigated after report by sum2012 in #16238. Not actually tested yet.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 10:33:08 -06:00
Henrik RydgårdandClaude Opus 5 45727ef2be Tighten the comments on the mpeg PRX changes
Cut restatement and asides that only made sense against earlier, wrong versions
of the code, and prefer parentheses over paired dashes. Also fix two comments
left stale by the descriptor rework, and record the rule in AGENTS.md.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 10:09:48 -06:00
Henrik RydgårdandClaude Opus 5 1dbd1afc20 Headless: forward the program's stdout/stderr to ours by default
Running a homebrew and seeing what it prints is the most basic thing
PPSSPPHeadless does, but sceIoWrite() to fd 1 and 2 only ever went into
the log, so it took -l to see any of it - which turns on every log
channel at debug level and buries the output.

The debug-output listener now gets a channel, and headless writes StdOut
and StdErr straight through to the host's, unmodified. With no listener
(the normal app) the old sanitized Log::Printf line is unchanged.

pspautotests writes exclusively to the "emulator:" devctl channel, so
nothing there moves; --compare and --bench suppress the forwarding along
with the debug channel, keeping test console output as it was.

Also fixes a potential one-byte OOB read in the same path when an
unmapped address clamps validSize to 0 with size > 0.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 09:59:37 -06:00
Henrik RydgårdandClaude Opus 5 e784f8bccb sceMpegbase: delay the colour conversion, as the hardware does
sceMpegBaseCscAvc/CscAvcRange run on the DMACPLUS and take real time. A
psmfplayer game re-blits the current video frame every render frame while it
waits for the next, so an instant return here is a tight loop that never yields
and starves the audio thread the playback clock is paced by - the whole A/V
pipeline then deadlocks a few frames into the movie. SOCOM: Tactical Strike
hung exactly this way running the real mpeg.prx; with the delay it plays. Same
value and reason as our sceMpeg HLE's sceMpegAvcCsc.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 09:53:03 -06:00
Henrik RydgårdandClaude Opus 5 4079adf424 sceVideocodec/sceMpegbase: fix the Media Engine frame descriptor
mpeg.prx reads the eight frame buffer addresses straight off the front of the
structure sceVideocodec publishes - `lw` at 0x00..0x1C, the same in Daxter's
disc copy (1.3, at 08805698) and in flash0:/kd/mpeg.prx (1.8, at 08805898) -
and takes the dimensions from its own context. We were writing the dimensions
at 0x00/0x04 and the buffers at 0x10, so slots 0 and 1 received 17 and 30 and
the four chroma addresses never arrived at all.

That survived in Daxter only by cancelling out: mpeg.prx hands the same words
back in the descriptor it builds for the colour conversion, which read them
with the same skew. It bites as soon as they are used as real addresses.

So also:

- sceMpegBaseYCrCbCopy moves the frame, rather than copying 48 bytes of
  descriptor over the caller's table of destination pointers. mpegbase.prx
  builds a DMA list over the eight buffers (080010f8 in mpegbase_260.prx):
  flags bit 0 takes 0,1,4,5 and bit 1 takes 2,3,6,7. mpeg.prx always passes 3.
- The chroma buffers are paired like the luma ones, left/right then even/odd
  rows, which is what that flag split assumes. Ours grouped them by half, so
  the per-buffer sizes disagreed with the caller's.
- The conversion reads the descriptor as mpegbase.prx does, and takes the
  buffers from wherever they are: still in the Media Engine, or already copied
  into the game's memory.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-16 21:32:36 -06:00
Henrik Rydgård d8a1830587 Merge pull request #22297 from kailashrs/gles-drawrangeelements
GLES: Pass known index range to glDrawRangeElements
2026-09-16 18:56:49 -06:00
Henrik Rydgård 409e078154 Merge pull request #22294 from hrydgard/real-mpeg-prx-fixes
Fixes to using "DisableHLE" for sceMpeg
2026-09-16 18:30:26 -06:00
Henrik RydgårdandClaude Opus 5 6bc20ab64b Had Claude clean up its own notes.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-15 11:21:02 -06:00
Henrik RydgårdandClaude Opus 5 992b8bd9b6 sceMpegbase: tell the GPU that the colour conversion wrote a frame
The CSC writes RGB straight into the display buffer, which the hardware
backends can't see on their own - our sceMpegAvcCsc HLE calls
PerformWriteFormattedFromMemory for exactly this reason, and the mpegbase
path didn't. Daxter's intro decoded normally with the screen frozen on the
menu behind it. Invisible under --graphics=software, which reads that memory
directly.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-15 11:06:56 -06:00
Henrik RydgårdandClaude Opus 5 16bf3a6519 BlockAllocator: let an allocator with no blocks save and load
An allocator that nothing has Init'd yet is a real state, not a broken one -
sceVideocodec keeps one for Media Engine memory that stays empty until a game
plays a video - but DoState asserted on bottom_ when writing, and on reading
treated a block count of zero as corrupt. Both ends handle it now, and the
write loop no longer special-cases the first block.

The stream layout is unchanged, so states written before this still load: they
always had at least one block, and take the same path they always did.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-15 11:06:56 -06:00
Henrik Rydgård 03bb3e20fe Merge pull request #22286 from hrydgard/real-mpeg-prx
Implement sceVideocodec, run sceMpeg as LLE on top (controlled by DisableHLE)
2026-09-14 17:23:01 -06:00
Henrik Rydgård 9e7f94272b Merge pull request #22288 from hrydgard/debugger-fixes
Win32 debugger: stop cutting off register values in CtrlRegisterList
2026-09-14 17:22:46 -06:00
Henrik Rydgård 21645bee54 Merge pull request #22290 from hrydgard/savestate-custom-names
Custom names for save states
2026-09-14 17:22:34 -06:00
Henrik Rydgård fa284ad087 Merge pull request #22287 from hrydgard/imgui-1.92
Update the Dear ImGui dependency to 1.92
2026-09-14 11:06:50 -06:00
Henrik Rydgård 202b78d95b AI-generated translation strings 2026-09-14 10:59:38 -06:00
Henrik Rydgård 10a9d7bb31 De-claude some overly verbose comments 2026-09-14 10:42:55 -06:00
Henrik RydgårdandClaude Opus 5 6600d1c05f Savestate browser: delete the screenshot and name file along with the state
Deleting a savestate from the savedata screen goes through GameInfo::Delete,
not SaveState::DeleteSlot, and it only knew about the .jpg - so the slot's
.name.txt was left behind with nothing to belong to.

Rather than teach the UI the naming scheme a third time, SaveState now answers
what sits beside a state.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 13:58:20 -06:00
Henrik RydgårdandClaude Opus 5 55f53d4523 Savestate names: read from the cached listing, and tidy up the name files
GetSlotCustomName hit the disk for every slot each time the pause screen built
its views, almost always for a file that isn't there. Rescan's listing already
knows, and HasSaveInSlot - which gates whether the name is even shown - reads
the same map, so this can't hide a name the old code would have found.

SetSlotCustomName now rescans, so a rename is visible without depending on the
pause screen happening to rescan on its way back.

Also: clearing a name deletes the file instead of leaving an empty one behind,
and the extension is "name.txt" rather than plain "txt", so an unrelated
"<prefix>_<slot>.txt" in the savestate folder isn't read as a slot name.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 13:58:20 -06:00
Henrik Rydgård 70bab7f25c Merge pull request #22285 from hrydgard/improve-with-replaced-extensions
Path: make WithReplacedExtension(old, new) report a mismatch
2026-09-12 13:57:41 -06:00
Henrik Rydgård c81bc5c212 Win32 debugger: stop cutting off register values in CtrlRegisterList
The name and value columns were hardcoded at x=17 and x=77. The control has
a fixed width in the dialog and can not be widened, so the 8 hex digits of a
GPR only just fit - and once the register list got a vertical scrollbar, the
~17px it takes off the client width pushed the last digits off the edge.

Derive the value column from the client width each paint instead, pulling it
in far enough for a whole value to fit and clipping anything longer rather
than letting it spill. Same for the category tab labels.

Float registers print with %g rather than %f: in a column that narrow, a
clipped %f of a large value is worse than useless (1e20 would read as
100000000), while %g keeps six significant digits and stays short for the
values you normally see.
2026-09-12 13:46:12 -06:00
Henrik RydgårdandClaude Opus 5 70095e458b thin3d: add sub-rectangle texture updates, use them for imgui fonts
imgui 1.92 hands the backend a list of dirty rectangles when its font atlas
grows, but thin3d could only replace a whole mip level, so every new glyph
re-uploaded the entire atlas.

Adds DrawContext::UpdateTextureRegions, taking a batch of regions so each
backend can submit them together:

- Vulkan: packs the regions into the push pool and issues a single
  vkCmdCopyBufferToImage from the init command buffer, transitioning only
  the level being written.
- OpenGL: new TEXTURE_SUBIMAGE init step. The existing sub-image path is a
  render command needing an active render pass and a texture slot, which
  doesn't fit here. Rows are packed caller-side since GLES2 lacks
  GL_UNPACK_ROW_LENGTH.
- D3D11: UpdateSubresource with a box, no staging texture needed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 13:16:17 -06:00
Henrik RydgårdandClaude Opus 5 43850e7c03 Update Dear ImGui to 1.92.9b (docking)
From 947aa9c97 to 98a66d8c8 on the docking branch - the latest release there,
plus the viewport fixes that landed on top of it. The only local changes to
upstream files are the "#undef new" hack at the top of imgui.h and enabling
IMGUI_DISABLE_OBSOLETE_FUNCTIONS in imconfig.h; both re-applied.

1.92 reworked fonts and textures, so the thin3d backend now advertises
ImGuiBackendFlags_RendererHasTextures and creates/updates/destroys imgui's
textures on request instead of building the font atlas itself. ImTextureIDs
below 256 now index those, above it the per-frame temp textures as before.
thin3d can't replace part of a texture yet, so an update re-uploads the whole
thing - fine for an atlas that only changes when a new glyph appears.

Also: AddRect() swapped its thickness and flags parameters in 1.92.8.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 12:59:53 -06:00
Henrik Rydgård a6996f570f ImDebugger: make selected text more visible 2026-09-12 12:58:56 -06:00
Henrik RydgårdandClaude Opus 5 6110a27b73 sceVideocodec: one layout helper for the eight frame buffers
Four places were each computing the same sizes and 64-byte-aligned offsets:
the allocation, the recovery sceMpegbase uses, and the readers on both sides.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 9422cb4899 sceMpegbase: save its state
The output pixel mode decides both the colour packing and the bytes per pixel of
the conversion, and a game sets it once per movie rather than per frame - so a
state resumed mid-movie converted at the default until the next
sceMpegBaseCscInit, which may never come. The gathered PES payloads go in too,
since a state can land between the copy and the decode that consumes it.

The section is optional (minimum version 0), so states written before it still
load.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 9dfce3ec2f ImDebugger: list the sceVideocodec contexts alongside sceAudiocodec
Same shape as the audio table: one row per open context, with its EDRAM block
and frame buffer allocation. Both are in ME memory, so the addresses shown are
in that space, not in PSP RAM.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 4ae682283c Path: make WithReplacedExtension(old, new) report a mismatch instead of hiding it
It used to return the path unchanged when the path didn't end in oldExtension,
so a caller that guessed wrong silently went on using the original file - and
"the screenshot next to this savestate" quietly becomes "this savestate".
Every caller had to know to check the extension first, and most didn't.

Now it's [[nodiscard]] bool with an out-param, in the style of ComputePathTo
next door, so the mismatch has to be handled. Changing the signature rather
than the behaviour means no call site can keep the old assumption by accident.
All four callers wanted "skip it" or "fall back", which they now say out loud.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 11:54:11 -06:00
Henrik Rydgård 90a85e235b Merge pull request #22284 from hrydgard/real-module-swap
Clean up and fix some issues with DisableHLE flags
2026-09-12 11:36:43 -06:00
Henrik RydgårdandClaude Opus 5 7bebc8db21 Translate the missing-firmware warning
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 11:11:51 -06:00
Henrik RydgårdandClaude Opus 5 4df01fd990 sceUtility: load the firmware's module for a disabled HLE library
Doing this was written once, for sceMp4, remembering the two UIDs it had
loaded in a static. That static was the only thing stopping a second copy
being loaded on top of the first, and it didn't survive a savestate: resuming
in a fresh process left it at zero while the restored module list already had
the modules in it. Ask the module list instead, which needs no state of its
own and is right after a savestate load and across games.

With the mechanism shared, sceMp3 and sceAtrac get it too. libmp3.prx and
libatrac3plus.prx each import nothing but the kernel and sceAudiocodec, so
both run against what we already have. The sceAtrac checkbox previously led
to an error log and a debug assert saying it wouldn't work, with a note that
we could go and find the file - which is what this does.

This is only ever the path for a game that ships no copy of the library. One
that does loads it directly and never asks sceUtility for it.

Also:
 - a module we're really loading no longer reserves its stand-in block as
   well. That block only stands in for memory we aren't otherwise taking, so
   reserving both charges the game twice, at the top of user memory where
   thread stacks come from.
 - the module loads at the bottom of the user partition. fromTop is for a
   module injected before the game's executable is placed; by the time a game
   calls sceUtility nothing moves either way, and the firmware's utility.prx
   allocates AV module memory as PSP_SMEM_Low.
 - NotifyLoadStatusAtrac read the raw setting rather than the effective
   flags, so it could fire for a flag that wasn't in effect.
 - the missing-firmware case now says so on screen, naming the library and
   pointing at installing a firmware, rather than only logging.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 11:11:41 -06:00
Henrik Rydgård 3ddde0fdfe Merge pull request #22282 from hrydgard/graduate-simple-libraries
Run sceFont, sceDeflt, and a bunch more libraries directly without HLE
2026-09-12 08:50:29 -06:00
Henrik Rydgård b693490ced De-claude some comments 2026-09-11 21:24:18 -06:00
Henrik RydgårdandClaude Opus 5 50d00541f9 headless: run graduated modules for real on a disc, and add --firmware-from-disc
Headless force-enabled HLE for everything the caller didn't name, so a run never
exercised the graduated modules the way the app does. That is right for
pspautotests, which is homebrew PRXes shipping none of the user libraries a
retail disc carries - scePsmfPlayer and friends would have nothing real to run.
It is wrong for a disc, which brings its own copies. Decide from the resolved
boot list: a test batch, or a --vsh run with no file at all, keeps the homebrew
treatment; exactly one non-executable target is a disc and is left alone.

--firmware-from-disc installs the firmware most discs carry into a scratch NAND
beside the memstick and boots against it, which is the version that game shipped
with. Kept per disc so a second run reuses it. It goes after the point where the
NAND root is settled, since that assignment is unconditional and would otherwise
overwrite it - which it silently did until the sceFont check started depending
on the answer.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-11 21:09:49 -06:00
Henrik RydgårdandClaude Opus 5 039ffc682d HLE: graduate the leaf libraries games carry on the disc
A survey of disc images found these shipped on a lot of them: LIBDEFLT and
LIBMT19937, LIBADLER, LIBMD5 on about 1/6, LIBSHA256 on 1/7, LIBHEAP on
1/12, LIBSFMT19937 on 1/24. None appears in any firmware dump and none is loadable
through sceUtility, so a game that imports one has to carry it - which makes
running the game's own module the safe default, the same argument psmfplayer
graduated on. Read off real discs with --re-module disc0:/..., all but sceHeap
import nothing at all, and sceHeap only Kernel_Library and ThreadManForUser.

sceFont joins them with a condition. Its module is on the disc like the others -
the most-shipped of all, on a third of the discs - but it reads its fonts from
flash0:/font and has nothing to fall back on, so it is only usable with a
firmware dump installed (our own fallback fonts are just not good enough anyway, it
chokes on them). CheckDisableHLEAvailability puts the HLE back when
those fonts are missing, since ours does have a fallback.

sceParseUri and sceParseHttp look like candidates and aren't: they are in the
firmware and sceUtility can load them, so a game may import them without
shipping one. Their headers say so.

The HLE implementations stay - old savestates index these tables by syscall
opcode - and are marked legacy the way scePsmf is.

Also: Move HLEInit after the filesystems are mounted

It ran before MountFileSystems, so the checks in it deciding whether a library
can run its real module instead of our HLE couldn't ask the PSP filesystem and
had to reach for host paths under the NAND directory. Nothing between
CoreTiming::Init and here touches HLE, and the kernel and the game's modules are
both loaded later, so it can simply move.

Two things fall out. The checks now spell their paths flash0:/... like the rest
of the emulator. And they run after AutoInstallFirmwareFromDisc, so a disc that
carries a firmware counts on the launch that installs it rather than the next
one - verified with an empty NAND and a disc carrying 3.11: the fonts land and
sceFont runs the game's own module the same boot.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-11 21:09:33 -06:00
Henrik RydgårdandClaude Opus 5 71c9b94772 re: let --re-module read a module out of a disc image
The interesting copy of a library is often the one a game ships rather than the
firmware's - and until now getting at it meant extracting the file by hand.
Naming it disc0:/PSP_GAME/USRDIR/MODULES/LIBDEFLT.PRX with the disc as the
positional argument mounts the disc and loads from there, alongside the existing
host paths and flash0: ones. No new option.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-11 10:30:54 -06:00
Henrik Rydgård 2e6fd06ed6 Merge pull request #22279 from hrydgard/audio-blocking-hardware-model
sceAudio: model the real buffering, so contending threads get told BUSY
2026-09-10 17:23:28 -06:00
Henrik RydgårdandClaude Opus 5 9a77ebb743 sceAudio: tighten up the rest of the savestate handling
A re-read of the whole thing after the reservation bug, looking for anything else
it got wrong.

The SRC channel's buffer count is read straight out of the state and then used to
index a two-element array, so a corrupt one is a write outside the struct. It is
range-checked now, the same way the channel count above it already was.

A channel holding a buffer with no samples left of it was stuck: the mixer skipped
it without ever clearing the address, so it read as busy for ever and the game had
no way back to sound. The API cannot produce that - a channel is never reserved
for zero samples - but a savestate can claim it, so the mixer now retires such a
channel instead of stepping over it.

Also took the "get rid of this next time we bump" the version-2 resampler section
came with, since this branch is that bump. Nothing was ever in it.

Checked by loading states written by a released build for a game that uses the
mixer channels and one that uses Output2, plus a round trip of the new format.
Every section ends in a marker, so a conversion path that consumed the wrong
number of bytes would fail the load rather than quietly corrupt what follows.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 c1bbd6ae2b sceAudio: stop dropping the reservation when loading an older savestate
Loading a state written by a released build left every channel unreserved, so
the game's next output came back SCE_ERROR_AUDIO_CHANNEL_NOT_INIT and the audio
never recovered.

The old format's reserved flag, sample count, volumes and format are read at the
top of AudioChannel::DoState and are perfectly good. Only the play position
cannot be reconstructed. The conversion path used clear() to zero the fields that
had no equivalent, which threw the rest away with them - my own cleanup two
commits ago swapped explicit field assignments for that call and did not notice.

The SRC channel had the same problem from the other end: older states carry it as
a ninth entry in the channel array, and that record was read into a throwaway.
What reserve agreed on carries over fine, so it now does.

Both halves were checked by loading a state written by a master build, with and
without the fix. Newer states are untouched and still round-trip.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 84bd8459e9 sceAudio: implement sceAudioOneshotOutput, and cover the rest of the channel rules
Another pass looking for gaps, all of it now recorded by audio/blocking/channels
and audio/blocking/oneshot.

sceAudioOneshotOutput was the last unimplemented entry in the module, returning
"library not linked" to anyone who called it. It plays one buffer on a channel it
never reserves, so the channel frees itself when the buffer runs out, and its
argument checks are their own set: any positive sample count, aligned or not, no
upper bound, a negative volume rejected rather than skipped, and no busy check at
all. No game is known to use it; it is implemented because tracing it turned out
to be cheap, not because anything needed it.

The channels test confirms four behaviors: sceAudioChReserve(-1) skips a released
channel that is still playing while a reserve of it succeeds, a second channel
joining a running mixer doesn't lose a block unlike a first, mono counts down in
the same 64-sample steps over the same time as stereo, and the panned blocking
output has no extra delay on the high channels.

Also: the SRC resampler now interpolates into the next buffer at a buffer join
instead of holding the last sample, since the codec reads the two descriptors as
one stream. That only shows up at non-native rates and no test can see it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 bfbede717f docs/sceAudio: record what vaudio does differently, and what a call costs
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 412d11534c sceAudio: drain on vaudio release, and charge what an output call really costs
Two things the last pass looked at and left, now measured on hardware by the new
audio/blocking/vaudio and audio/blocking/overhead.

sceVaudioChRelease is not shaped like the Output2 and SRC releases. It hands the
channel a null pointer first, which waits for a buffer to finish, so it blocks for
one buffer and returns 0 where the others would refuse - and the buffer is played
out instead of being dropped, which is what we were doing. The reservation goes
now rather than when the drain finishes: the caller is parked either way and gets
the same answer at the same time, and the buffers keep playing because the mixer
does not look at the reservation.

The same test turned up two more: a *failed* sceVaudioChReserve still marks vaudio
reserved, so a caller that lost the channel to Output2 is told 0x80000021 next
time until a release clears it; and sceVaudioChRelease ignores that flag entirely
and releases whatever holds the SRC channel, which pspautotests already called the
"wrong release".

The flat 10000 cycles charged to every Output2 and SRC output turns out to be
right only for the refused case. Every other outcome ends up querying the codec
and costs over 100us on hardware, including finding the channel unreserved, so
those now cost 25000. Mixer channels are the other way round - cheap to refuse,
expensive on the one output that starts the DMA - so that charge moved to the DMA
start. F1 2009 behaves identically and Burnout Dominator's mixed output is
bit-identical to before.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 000af25d81 sceAudio: split the SRC channel out, and stop rescheduling mid-syscall
The nine channels were one array of one struct, but only index 8 ever used the
two DMA descriptors, the played/fraction position and the waiting-thread vector,
and only 0-7 ever used the buffer slot - so eight copies of a std::vector sat
there dead. They are two different pieces of hardware and now they are two
structs: AudioChannel for the eight the mixer walks, and a single AudioSRCChannel
for the one the codec reads directly.

Found while rereading it:

- __AudioUpdate can now run from inside an output call, and it reschedules when
  it wakes a thread. A context switch there lands before the syscall writes its
  return value, so the caller loses it and the woken thread gets it instead.
  Deferred with hleReSchedule when a syscall is in flight. The same fix applies
  to the release path, which had the problem before this branch existed.
- A finished SRC buffer whose waiting thread had given up dropped the completion
  entirely, so the next caller waited a buffer too long. Now it walks past dead
  waiters and banks the completion if nobody is left.
- An output with a null pointer changed the channel volume, where the hardware
  returns before touching it.
- A busy channel came back as an error from the Output2 path and as debug from
  the mixer path. It is an ordinary answer a game polls on, so both are debug
  now; the old behavior filled the log with 18k error lines in a minute of F1
  2009.
- AudioChannel::reset had no callers left: releasing a channel with a thread
  parked on it is refused, so there is nobody to wake.
- The two routing modes are globals and were written once per channel. They get
  their own small savestate block.

Savestate: the sceAudio section goes to 3, and the SRC channel gets a section of
its own. States from released builds carry it as a ninth channel record, which is
read and discarded.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 ac50cd5759 sceAudio: model the real buffering, so contending threads get told "busy"
I deduced that this was the case, and attempted implementing this path long ago,
but I could never quite get it to work in all games. Set Claude on a quest to
research and implement it, and lo and behold, it works. A bit sobering.

Fixes #12888 and likely more. Additionally, audio latency is likely slightly
improved overall, and memory usage is down by 4.6MB.

Claude says:

The blocking output calls are not a queue that callers line up behind. Each
mixer channel holds exactly one buffer and at most one parked thread; a second
thread arriving while the first is waiting is told the channel is busy and is
expected to skip its turn. The Output2/SRC channel holds two DMA descriptors and
refuses a third caller outright, without waiting at all.

We blocked everyone instead, so a game running a movie thread and a sound-effect
thread over one output made the two alternate - a frame of movie audio, a frame
of effects silence - and the movie played at half rate. That is #12888, seen in
F1 2009 and Colin McRae: DiRT 2. With this, the movie thread keeps the channel
for the whole cutscene and the effects thread is refused, which is what the
hardware trace shows.

The driver also never copies a buffer on the way in: it stores the pointer and
its mixer walks it forward 64 samples at a time out of the game's own memory.
Modelling that fixes #20095 as a side effect, and drops the 4.6MB of per-channel
sample rings we were carrying. The mix event is re-phased to the moment a DMA
starts, since the mixer thread outranks its caller and gets a block in before the
output call returns.

Along the way: the two rest-length calls differ after a null-pointer output,
sceAudioChRelease reports not-reserved rather than not-init,
sceAudioChangeChannelConfig validates the format,
sceAudioChangeChannelVolume validates nothing, and sceAudioOutput2ChangeLength
takes a range of 17..4111. Details in docs/sceAudio.md.

Savestates: AudioChannel goes to version 4. Older ones stored mixed samples that
can't become a pointer and a position again, so they load with the pending audio
dropped and any parked threads released.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 17:03:07 -06:00
Henrik Rydgård 6fa654fce2 Merge pull request #22277 from hrydgard/kernel-mode-partitions
Kernel mode partitions and sceIo behavior
2026-09-10 17:02:50 -06:00
Henrik Rydgård 76d574ded7 Merge pull request #22278 from hrydgard/firmware-overwrite
Auto-install PSP firmware from game updaters on game launch
2026-09-10 16:47:16 -06:00
Henrik Rydgård 21fa7f10e1 Minor fixes 2026-09-10 16:04:39 -06:00
Henrik Rydgård f0920acef7 Bump gradle 2026-09-10 15:52:53 -06:00
Henrik RydgårdandClaude Opus 5 4a51602adf Translate the firmware auto-install strings
The five new [System] keys, in the 33 languages that already had a rendering of
"firmware" to follow - they disagree on it (fastvare, prosjivka, systemprogramvara,
laiteohjelmisto), so each one matches whatever its own file already chose. The
other 13 fall back to English.

Firmware screen: call it "Uninstall firmware", not "Erase firmware"

Translate the rest of the firmware screen strings

"Uninstall firmware", "No firmware installed" and "Launch XMB", in the same 33
languages as the auto-install strings. XMB stays untranslated - it's Sony's
name for it, and pl_PL already wrote it bare.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 15:52:53 -06:00
Henrik RydgårdandClaude Opus 5 ad06cbe2c4 Auto-install firmware from the game disc on boot
Most UMDs carry a firmware updater, and having a real firmware in the NAND is
what the LLE modules want. On boot, if the disc's updater is newer than what's
installed - or nothing identifiable is installed, which is what a fonts-only
NAND looks like - unpack it, with a progress bar on the OSD. Controlled by
bAutoUpgradeFirmware, on by default, with a checkbox on the firmware screen.
Off in headless, which shouldn't rewrite the NAND during a test run.

The install itself is now shared with the install screen, and hardened, since
it can run without anyone watching:

- It stages into <NAND>/install-staging and only erases the installed firmware
  once the new one is complete on disk, so a failure leaves what's there alone.
- A single entry that didn't unpack fails the whole install. A firmware with
  holes still looks installed, so nothing would ever replace it.
- A truncated archive is rejected instead of unpacking to half a firmware that
  every stat reports as a clean install. The PSAR header records the length;
  it was being clamped to the buffer rather than checked against it.

Also falls back to the version in the archive's own header when the PARAM.SFO
next to the updater is unreadable, which it is on a fair number of discs.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 15:52:53 -06:00
Henrik Rydgård d6c0a7bbde Firmware install: Change dialog layout, remove warning when not needed 2026-09-10 15:52:53 -06:00
Henrik RydgårdandClaude Opus 5 e4d940604e Name both versions in the firmware overwrite warning
"Firmware 6.20 is installed. It will be erased and replaced with 6.60." is the
thing worth double-checking before wiping a firmware - installing off whatever
disc is to hand makes going backwards easy to do by accident.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 15:52:53 -06:00