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
Headless hardcoded its memory stick to "memstick" next to the executable, so
testing a real game there meant copying the game in - 85MB for the one that
prompted this. --memstick=DIR points it at any directory with the usual
PSP/GAME layout, including the one the app build already uses, so the two can
share.
Two things worth noting in the implementation. The headless default is applied
*after* CommandLineOptions::ApplyToConfig() runs, so it has to check whether
the flag was given - otherwise it silently overwrites it, which is how the
first version of this failed: the game still booted (its path was absolute) but
from the wrong stick. And there's no DoNotSaveSetting() call for
memStickDirectory, unlike the settings around it: it isn't an ordinary setting
and never reaches ppsspp.ini, because the ini lives inside the memory stick
directory it would be describing.
Verified by deleting the copied game and booting it from the repo's memstick
with --memstick, running to exactly 2s of emulated time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
Module symbols are keyed by module+crc so they're shared by every game that
loads that module. But symbols the user attaches to addresses that aren't in
any module - the heap, the stack, scratchpad, a hardware register, typically
after a memory.search - describe one game's own memory layout and are worthless
to any other game. Those now go to PSP/SYSTEM/SYMBOLS/<gameID>_syms.ppsym.
They're module index 0 ("absolute"), which already round-trips through the
existing per-module code: GetModuleRelativeAddr/GetModuleAbsoluteAddr are
identity for it, so the file format is unchanged, just with absolute addresses.
SaveModuleSymbols only needed to stop requiring a ModuleEntry. Auto-load/save
hang off CPU_Init/CPU_Shutdown rather than module load/unload, gated on the same
bAutoSaveLoadSymbols setting - and deliberately not on SYSPROP_HAS_DEBUGGER,
which only the Windows port reports true for, so LoadSymbolsIfSupported next to
it does nothing at all on headless. hle.game.saveSymbols/loadSymbols expose it.
Four things found while doing it:
- Symbols outside any module were being dropped on the floor. AddFunction/
AddData/AddLabel take moduleIndex -1 as "work it out", pass it to
GetModuleIndex(), and store whatever comes back - but that returns -1 when no
module contains the address, and -1 is never an active module, so the symbol
never reached the active maps: invisible to every lookup and to any save.
hle.data.add had spotted this and normalized -1 to 0 locally; nothing else
did, so e.g. hle.func.add outside a module silently did nothing. Fixed
centrally in a new ResolveModuleIndex() the three of them share.
(This only became reachable with the GetModuleIndex() fix in 29a38af37e -
before that it returned a wrong-but-valid module index instead.)
- The saved files were almost entirely noise. Every function the analyzer finds
is named z_un_<addr> and every import stub zz_<name>, both regenerated from
scratch on each load. One real module wrote 13KB - 443 unnamed functions and
64 stub names - for the four names a human had actually chosen. Worse, on the
next run those were loaded back as authoritative and would beat the module's
own symbols to the address. Now only names that aren't regenerated get saved,
and a module with none writes no file at all (and removes a stale one, so
deleting a symbol sticks). That module's file went 13020 -> 81 bytes.
- LoadModuleSymbols trusted the addresses in the file. It's meant to be
hand-edited and can outlive the build it came from, so relative addresses past
the end of the module are now skipped with a warning instead of landing at
nonsense addresses.
- AddFunction and AddData both erased the map entry they were updating and then
read back through the now-dangling iterator to refresh the active copy. Only
latent: the refresh is guarded on the active copy's module matching the new
one, which is false exactly when the erase happens. Re-point the iterator at
the entry's new home instead, so it can't rot if that guard ever changes.
AddLabel already did the equivalent correctly, via a local copy.
Filename sanitizing goes through SanitizeString with a new FileName restriction
rather than being open-coded; unlike the existing restrictions it substitutes
'_' instead of dropping, so two module names can't collapse onto one file.
Verified end to end on headless with cpu_alu.prx: named a function inside the
module and data/functions in scratchpad and the heap, let it exit, checked both
files, rebooted and confirmed all of it came back at the right addresses.
Unit tests 51/51, pspautotests 314/314.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
SymbolMap:
- Fix GetModuleIndex(): it only checked the end of an active module's range
(via activeModuleEnds.upper_bound), never the start, so an address sitting
in the gap before a module was silently misattributed to it. Added
GetModuleIndexByName() as a companion lookup.
- AddModule() gains an optional crc param, stored per ModuleEntry. Reactivating
a module by name now also requires the crc to agree when both sides know it,
so two unrelated binaries that happen to share a name no longer get merged
into one symbol table (addresses the old TODO at the top of SymbolMap.h).
- AddLabel()/AddFunction() gain an updateName param (default false, preserving
existing "first writer wins" behavior) so a trusted source - like a loaded
symbol file - can be allowed to overwrite a name that a lower-confidence
automatic pass already assigned.
- New SaveModuleSymbols()/LoadModuleSymbols()/GetModuleSymbolsPath(): save or
restore one module's functions/data/labels to/from a small human-editable
text file, addressed relative to the module (so the file stays valid however
the module ends up positioned on a later run). Keyed by
PSP/SYSTEM/SYMBOLS/<moduleName>_<crc>.ppsym - deliberately by module+crc
rather than by game, so it's shared by every game/homebrew that loads the
exact same module. A "# game <id> <title>" comment records who last saved
it, informational only.
WebSocket debugger: hle.module.saveSymbols/loadSymbols expose the above.
sceKernelModule.cpp: auto-load a module's saved symbols right after it's
registered with the symbol map (both the real ELF-load path and the
savestate-load path), and auto-save on unload (before UnloadModule(), while
its symbols are still active) - gated behind the new bAutoSaveLoadSymbols
config setting (default off), with a matching Developer Tools checkbox and
a --auto-save-load-symbols command-line override for headless use.
Includes some in-progress cleanup already staged: DescribeAddress now calls
g_symbolMap->GetDescription() directly instead of through the now-removed
MIPSDebugInterface::getDescription() wrapper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
MIPSTables.cpp has walked a tree of tables on every single interpreted
instruction since forever, with a standing TODO asking for exactly this:
"generate smart dispatcher functions from above tables instead of this
slow method." GenerateInterpreterDispatch() does that - it walks the
same tables MIPSGetInstruction() walks at runtime, but resolves the
walk into a nested switch tree once, at generation time, with each
leaf calling straight into the existing MIPSInt::Int_* handlers and
returning that instruction's fixed cycle count. Anything not covered
(invalid opcodes, and the handful of instructions with no interpreter
implemented at all, e.g. tge/tlt/teq) falls back to the existing
MIPSInterpret()/MIPSGetInstructionCycleEstimate() slow path, so the
result is total over all 32-bit inputs, same as the table-walking path.
Wired up via a new headless --generate-interpreter-dispatch flag,
which prints the generated Core/MIPS/InterpreterDispatch.cpp source to
stdout and exits.
Also widens CmdLine.cpp's --help column formatting, which silently
truncated any option name longer than 24 characters - the new option's
name was the first to hit it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SKhm9wKEQzRUsx9mTrrtQ
Games often use the framebuffer alpha channel for non-visual purposes,
so saved PNGs could look fully transparent in image viewers. Force alpha
to 255 when writing PNG screenshots unless --screenshot-keep-alpha is
passed. The MSE comparison ignores alpha either way.
- Add frametests.py: walks a dump tree, renders each dump per config variant
through PPSSPPHeadless, generates reference images when missing and compares
MSE when present, and writes a self-contained HTML report. The JSON config
(which lives with the test set, not in the repo) points at the data tree
and defines variants as suffix -> CLI args, e.g. 'soft': '--graphics=software'.
- Headless: --screenshot-save saves PNG when the path ends in .png; new
--screenshot-diff always writes a visual comparison when comparing;
screenshot comparison failures (mismatch or unloadable reference) now fail
the test instead of passing silently.
- Read back framebuffers top-down, flipping only for BMP output/input
(fixes upside-down PNG references). Sync libretro copy accordingly.
- Document the system in docs/frametest.md; add AGENTS.md reference.
Headless.cpp, NativeApp.cpp, and SDLMain.cpp each still hand-parsed a few
argv flags directly (mount/log/state/ignore/loglevel in headless and the
app, xres/yres/dpi/scale in SDL), duplicating and in some cases conflicting
with the shared CommandLineOptions parser. Consolidate all of it into
CmdLine.cpp/.h so there's a single source of truth, and drop the now-dead
remain_argc/remain_argv filtering in SDLMain.cpp since NativeInit no longer
reads argv itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PfFvWzpHxErWgRhKqqSewN
The headless build's own --debugger=PORT never actually worked on
Windows: headless/Headless.cpp never called net::Init() (WSAStartup),
so socket binding silently failed ("Unable to listen on any port
(debugger - webserver)"). Fixed by calling net::Init()/net::Shutdown()
there, gated on --debugger being passed since headless has no other use
for networking.
With that confirmed working, move parsing into the shared
Core/CmdLine.cpp auto-param table as a single --debugger=PORT option
(0 = pick automatically) available on both CmdLineMode::Application and
CmdLineMode::Headless, replacing the previous app-only boolean
--debugger flag and headless's separate ad hoc argv scan for it.
Behavior differs deliberately by build, same as before:
- App: just starts the debugger, game boots and runs normally.
- Headless: also forces coreParameter.startBreak = true (break before
anything runs), as it always has. Headless re-applies iRemoteISOPort
after its own g_Config.RestoreDefaults() call, which runs after
ApplyToConfig() and would otherwise wipe the requested port.
Verified live: headless now binds the requested port and responds to
cpu.status/game.status; the app build binds the requested port and
runs the game normally (not paused).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
Application-mode only (CmdLineMode::Application) - the headless build has
its own separate --debugger=PORT mechanism in headless/Headless.cpp, which
doesn't currently work reliably, so this new flag intentionally does not
apply there. Sets bRemoteDebuggerOnStartup for the session without
persisting it to the config file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6