--mount ISO used to be headless-only, and EmuScreen always cleared
CoreParameter::mountIso. Pass it from the command line into the first
EmuScreen, which keeps it across resets and drops it when another game
is booted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
-i, -j, -r, -J, --ir and --jit-ir are gone. -r was also --root's short form,
and --root won, so -r took the next argument as the root path and nothing
booted (test.py -r failed every test).
-i, -j and -J now fail with an error pointing at --cpu= instead of quietly
running on the default core. Docs and test.py updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--press=SECONDS:MASK[:DURATION],... holds sceCtrl buttons at emulated
times, and --screenshot-at=SECONDS:FILE,... saves screenshots along the way,
to drive a game past its menus in a run. With presses, the run loop steps
1/60 of a second at a time so short ones land.
MeCore owns a second MIPSState in the ME's address space, held in reset
until released (SetReset). It runs on the interpreter, a slice behind the
main CPU: CoreTiming runs it for the cycles the main CPU has just used, in
Advance and ForceCheck. Its own PRId (COP0 22) and mfic/mtic flag; MMIO
counts as kernel mode on it, and a syscall stops it with an error rather
than reaching HLE. The interpreter's block stepping reads delay slots
through the stepping CPU's memory, and cache instructions log at verbose
level, as ME code flushes its caches in a loop.
Created at boot only for homebrew (PBP with MG category or no SFO, ELF;
the VSH counts, as vshmain.prx loads as an ELF), when "Enable ME
emulation for homebrew" is on: a global checkbox in developer settings,
off by default and not translated yet, or --me-core. Saved in its own
section after MMIO (SaveStart v5), with its own memory; loading a state
creates or removes the ME to match.
The boot page now starts with a reset stub that sends the ME on to
0xBFC00040, where homebrew puts its code (from the firmware's reset code,
read on a PSP-1000).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#18456
For external launchers (Android handhelds like the Retroid Pocket), which want
control back when the game ends. Before, only the pause menu's Exit quit the
app; now the game exiting by itself, a stop request and dismissing a load
error do too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Selectable with --graphics=metal, the backend choice in settings and the
Mac menu bar. SDL creates a Metal view for it, and headless renders
offscreen. Games run on the null GPU until there's a GPU/Metal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#12082
The bit-exact VFPU dot product is the default everywhere and fast enough,
so the plain multiply-add paths in the interpreter and the IR frontend go,
along with the AccurateVdot setting, --accurate-vdot, the ForceAccurateVdot
compat flag and the Vec4Dot IR op, which nothing emits any more.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
iOS only runs app bundles, so with HEADLESS=ON the iOS build makes
PPSSPPHeadless an app (org.ppsspp.headless) that starts UIKit and runs
the headless main on a thread with the arguments devicectl launched it
with. Vulkan only, and the IR interpreter since there's no JIT.
frametests.py --ios installs it, copies over the dumps the device lacks,
and compares each render with the host's Vulkan one like --adb does.
Each devicectl call costs a second or two, so the new --screenshot-dir
lets one launch render a batch of dumps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Overrides the locked CPU speed setting without saving it, so a headless run
can reproduce a game at a different frame rate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Turns off loading and saving the per-game shader cache for one run, so a
run can time shader and pipeline compiles like a first boot.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GPU_Null derives straight from GPUCommon and has no thin3d context, framebuffer
manager, texture cache or shaders. It runs display lists with the flow control,
bounding boxes, matrix loads (the hardware backends'), immediate vertices and
block transfers of the others, moves the vertex and index pointers past each
draw and curve, and charges the per-vertex and fill cost the software renderer
does. A DrawEngineCommon that queues nothing provides the vertex decoders.
It's for measuring the CPU emulation. From savestates, God of War, Tekken 6,
Wipeout Pure and LocoRoco flip the same frames in 10 emulated seconds as with
Vulkan and software, in 44-77% fewer instructions than Vulkan.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The native IR JITs get the emulator thread's host PC sampled every 50 us (a
signal on macOS and Linux, SuspendThread on Windows), mapped to blocks, the
routines they call and host functions. The report has each hot block's MIPS,
IR and host code with the samples per host instruction. The IR interpreter's
block timing, previously behind IR_PROFILING, now runs on the same switch.
Both cost nothing when off.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Game runs in headless measured the C++ vertex decoder, which the app only
uses where it can't JIT. pspautotests and the frametests pass with the
JIT decoders. --vertex-jit=false brings back the C++ decoder.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The accurate dot products cost a few percent at most in the IR JIT, apart
from GTA (about 20% on a game that's cheap to run anyway). The fuzzer follows
the default too, unless PPSSPP_CPUFUZZ_ACCURATE_VDOT=0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Not wired to anything yet. It will make every core compute the VFPU's dot
products (vdot, vmmul, vtfm and the rest) bit-exactly like the PSP, through
vfpu_dot().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Fixes#12343
Killzone: Liberation's Chapter 5 DLC is an installer that the PSP turns
into ms0:/PSP/GAME/<id>/KZL.PRX, which the game loads with
sceKernelLoadModuleDNAS. That file is a PGD around a type 3 PRX (decrypt
mode 18), both keyed to the console that installed it. Its Fuse ID
gives everything needed:
- libkirk: the per-console key derivation, and KIRK commands 3, 5 and 8
built on it (KIRK ROM per uofw, key mesh per psdevwiki).
- PrxDecrypter: type 3, which is type 5 with the first step through
KIRK command 3 (JPCSP's PRX.java), and the DNAS PGD unwrap.
- sceKernelLoadModuleDNAS uses the new FuseID setting (or --fuse-id).
Without one, or with the wrong one, the game falls back to its stock
module and an OSD message says why.
- Headless --re-decrypt takes --re-decrypt-key and --fuse-id, to decrypt
such a file offline.
Verified with a patch installed on a real PSP: the v1.20 module loads,
starts and reads CHAPTER5.BIN. Type 5 output is unchanged (an NPJH50521
PBOOT.PBP decrypts byte-identically). How to install one over PSPLink is
in docs/dnas-patches.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mali-G71 and PowerVR GE8320 Vulkan lack it, and without it WipEout Pure,
Juiced 2 and 16131 UCES00001 render like they do there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--disable-gpu-features=clip-distance,cull-distance,depth-clamp acts as if the
GPU lacked those, to test the fallbacks for old Mali on any machine. It
replaces the simulateOldMali constant. --hardware-transform=false runs every
draw through software transform.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An inverse boolean option was inverted twice, so --windowed forced fullscreen
on. --no-memstick runs with the memory stick removed, for testing that from
headless (which otherwise always inserts one).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headless leaves a game's compat.ini settings out when replaying a frame
dump, so comparisons with the PSP see what the renderer itself does. This
puts them back, for checking what a compat entry does to a dump (Puyo Puyo
Fever 2's depth rounding).
Co-Authored-By: Claude Opus 5.5 <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>
--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>
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>
An invalid value (e.g. --debugger-run swallowing the next flag) printed an
error but returned Exit, so the process exited 0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--disable-hle had no counterpart, which made "is this our fault or the game's?"
awkward to answer: the only ways to put our HLE back were a per-game config or
hiding the firmware, and neither works from a script - the setting is per-game
and the firmware gets found anyway. --force-hle takes the same bitmask and runs
our HLE for those libraries even where the real module is now the default, so
the same repro can be run both ways and the logs diffed.
Used it on the warnings left over in Tekken 6 under the real mpeg.prx. Three of
them appear identically with --force-hle=16, so they are the game's own and
match what the hardware answers: sceKernelChangeThreadPriority(-1) eight times
in a row (pspautotests/threads/threads/change says hardware returns
UNKNOWN_THID for -1 too), sceAtracAddStreamData on a released id with a zero
byte count, and a sceKernelDeleteMutex on a garbage id.
The fourth only happens under the real module and is ours. mpeg.prx sizes its
allocation through a scratch context in its own bss and calls
sceAudiocodecReleaseEDRAM on that one, while decoding through a different
context that never gets released - so the next movie's sceAudiocodecInit finds
a live decoder and replaces it. That is once per video on every game running
the real module, and it was a WARN_LOG_REPORT, so it would have reported from
everyone's machine. It is bounded - removeDecoder deletes the old one and Init
makes exactly one more - so it is an INFO_LOG now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
--nand had a field and an ApplyToConfig branch but nothing ever parsed it, so it
was silently ignored.
--screenshot-save only fired when a GE replay finished or a test used the
EMIT_SCREENSHOT devctl, and even then only under --compare. Anything else - a
game, or --vsh - ran to the timeout and wrote nothing. Capture the display at
the end of the run when nothing else did, which is what makes it usable for
looking at what a booting system actually has on screen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--debugger sets startBreak, so the run sits at the entry point until a client
resumes it. A session that forgets to do that looks like a frozen game rather
than a paused CPU - all the way down to "ticks: 0" - so say so on the way up,
and add --debugger-run for the common case of wanting the debugger attached to
a run that just goes.
Also stop headless forcing HLE for the graduated modules. Those come out of the
game's own disc rather than a firmware dump - libpsmfplayer.prx and friends are
user libraries, always present - so forcing them to HLE made headless quietly
disagree with the app about which code a game runs. Tekken 6 plays its movie
through scePsmfPlayer, and headless was faking it, so the movie never reached
sceMpeg at all; now the disc's real psmfplayer drives the real mpeg.prx.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--re-decrypt runs pspDecryptPRX() over a file and unpacks the KL4E/KL3E
stream behind it. This opens up flash0:/kd/resource/*.img, the images the
Media Engine actually runs: they are ordinary tagged containers (tag
862648D1, which PrxDecrypter already has a key for) with the ~PSP
signature blanked, so the normal module loader never touches them.
--re-raw-base analyzes --re-module as a flat code image at a given
address rather than as a PRX. The decrypted ME images are raw MIPS with
no ELF around them; the address they were linked for is recoverable from
their own jal targets (0x08300000 for meimg.img).
Also makes PrxDecrypter.h self-contained - PSP_Header is built from _le
types, so it needs Common/Swap.h rather than relying on the includer.
Loads a single module standalone - no game, no boot - and writes a report:
module header and segments, the export and import tables with NIDs resolved to
names through the HLE tables, one annotated disassembly file per function, and
a call graph as JSON.
Reuses the emulator's own loader rather than parsing PRXes a second time, so
decryption, decompression, relocation and import resolution can't drift from
what actually runs. Only enough of the system is brought up to load a module:
memory map, timing, HLE tables and the kernel allocators. Nothing executes.
Two things beyond a plain disassembly, both aimed at the questions that come up
when reading unfamiliar MIPS:
- lui/addiu (and lui/load) pairs are folded and reported as the address they
form, which is how every global and constant table gets reached.
- Per function, a register evidence block instead of a guessed signature. A
MIPS function that takes two arguments and passes the second one down often
never reads it, so 'never read but live across a call' is reported as
forwarded rather than quietly dropped from the signature.
SetForceRealModuleLoads() is needed because modules like sceAudiocodec_Driver
have no DisableHLEFlags bit and so can't be turned off the normal way - they'd
fake-load and there would be nothing to look at.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PSP game updates were distributed as NPDRM .pkg files holding a patched
EBOOT (PBOOT.PBP) plus the data files the patch replaces. PkgUnpack reads
one: header, item table, both PARAM.SFOs, and the AES-128-CTR that covers
everything past the header - including the per-item key split, where an
item's pspType byte picks between the PSP and PS3 keys.
Nothing new is needed to decrypt these. All packages checked use PRX
tag 0x2E5E10F0 for their PBOOT, which PrxDecrypter already has a key for.
Also adds "PPSSPPHeadless --install-pkg=DIR", a sibling of --unpack-updater,
which installs without any UI. All packages install through it byte
-identically to a reference implementation.
Format notes are in docs/pkg_notes.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The MP4 libraries turn out to be the easiest place to hand a game Sony's own
code: libmp4.prx needs only sceAudiocodecInit and sceAudiocodecDecode from us
plus ordinary kernel calls, and mp4msv.prx - where the 41 functions libmp4
leans on live - imports nothing at all. So with a firmware dump present the
pair can be loaded for real and left to decode through our sceAudiocodec.
Adds DisableHLEFlags::sceMp4, which loads and starts both modules when the
game asks sceUtility for the MP4 module, and a --disable-hle bitmask so a
headless run can ask for this without a config file.
Two things had to be fixed to make it work at all:
- ModuleMgrForUser 0xD2FBC957 was unimplemented, and libmp4 calls it to get
the gp of each callback it is handed. Implemented as
sceKernelGetModuleGPByAddress.
- Headless forced every module to HLE unconditionally, which silently undid
the flag, and it did so before ApplyToConfig() had even parsed it.
Tested with Speedball 2 - Evolution, which uses sceMp4 for its music: the game
goes from 255645 calls into our stubs and 39 unresolved imports, to zero of
each and 1943 AAC frames decoded through sceAudiocodec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decrypting an entry's contents is by far the most expensive part of
walking an archive, and it happened for every entry before the filter
was even consulted. Now it waits until entryData()/entryCompression()
asks, so pulling just the fonts out of an updater no longer costs a
full firmware decrypt. Records are decrypted independently of each
other, so deferring one is safe.
Unpacking fonts from a 3.11 updater goes 0.365s -> 0.133s; a full
unpack is unchanged and produces identical output. The compression
counts now describe the entries we actually decoded rather than
everything in the archive.
Also adds --unpack-updater-filter to headless, which is how the above
was measured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
--state could load a savestate but nothing could produce one without a
GUI, so savestate bugs couldn't be reproduced or regression-tested from
a script. This saves one partway through the run, once the game is
actually up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
CmdLine used std::stoi/std::stod and Compatibility used stoi/stof, all of which
throw on junk with nothing catching them. 'PPSSPPHeadless --timeout=abc' aborted
the process, and so did a bad value in a [PostShaderSetting]-style compat section -
at startup, with no diagnostic. Parse with sscanf and report it: CmdLine already
has the pattern for this in its Bool case, and Config.cpp's ini reads were fixed
the same way earlier. A bad compat.ini entry now warns and keeps the default.
An updater carries one file list per hardware revision, and which one you
resolve names against decides both what a file is called and whether it's part
of that model's firmware at all. That was hardcoded to "first list that names
it", which is right for extracting everything but wrong for reproducing what a
particular console would have installed.
PSARUnpackOptions::model takes a PSPModelGeneration now, and the lists are kept
per model rather than merged. Any (the default) keeps the old behaviour;
anything else uses only that model's list and skips what it doesn't name.
--unpack-updater-model on headless takes "01g".."12g" or "any".
On the 6.61 updater: any gives 411 files, 03g gives 330 with 81 belonging to
other models, 01g gives 313 with 98. The difference is what it should be - 03g
has the _03g.prx variants and arib.pgf, 01g has neither.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
An updater's DATA.PSAR is a flat sequence of records - each is 0x150 bytes of
PRX-style encryption header, a 0x110 byte entry describing one file, and then
its compressed contents. So to get the files, you don't actually have to run it
and let it self-unpack - we can just do it.
Two steps per record. First "demangle": the 0x130 bytes at +0x20 are AES-CBC
encrypted on top of everything else and hide the PRX tag at +0xD0, so a KIRK
CMD7 pass with keyseed 0x55 comes first. Then the record is an ordinary PRX blob
for the decrypter we already have, once it knows the tag - 0x0E000000, which is
new here. Its key needs the kirk7 scramble applied, unlike every other key in
that table, which are stored already scrambled; hence the flag on TAG_INFO.
UnpackPSAR() takes a prefix filter, since the planned main use for this is pulling
flash0:/font out of an updater the user supplies (or from an ISO) rather than
extracting whole firmwares, although that can also be interesting for running
the VSH.
Tested on a 6.61 updater: 436 entries, all 418 files decrypt and decompress,
nothing fails. The contents are what they should be - 295 ~PSP modules, 61 PRF
files, 18 PGF fonts, and the encrypted XMB indices.
Two things it doesn't do yet. Every entry in that archive is named with a
five-digit token rather than a path; the real names live in tables 00001-00012
inside the archive itself, under their own separate encryption, so files come
out under the short name for now and the prefix filter can't match them.
And only the zlib compression format is currently supported.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9