69 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 cc23f4cf13 AGENTS.md: Hardware claims need a test, one PSP operation at a time
Also that unresolved scePsmfPlayer imports early in a game run are
expected.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-30 09:58:37 -06:00
Henrik RydgårdandClaude Opus 5.5 54e4be47aa CoreTiming: Grow the event table for events a savestate lacks
Taking the highest unused slot instead could steal one that a state event
restores later, as VBlankWake did to MicBlockingResume, which then had
nowhere to go. Also name the event in the assert.

AGENTS.md: When a savestate fails to load, suspect the branch first.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-30 09:06:30 -06:00
Henrik RydgårdandClaude Opus 5.5 24b71386ff Document that shader cache key changes need a CACHE_VERSION bump
The OpenGL and Vulkan shader caches store raw shader IDs (and, for Vulkan,
pipeline keys) on disk. Add the rule to AGENTS.md and point to it from the
persisted types.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-29 09:35:17 -06:00
Henrik RydgårdandClaude Opus 5.5 0c70d77573 AGENTS.md: Keep headless game runs short and fast
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-28 10:57:21 -06:00
Henrik RydgårdandClaude Opus 5 772f4f6e7a AGENTS.md: note that headless needs the app's memstick for firmware runs
Without --memstick, a library whose HLE the config has disabled has no firmware
to resolve against, and the game dies on unresolved imports rather than falling
back to our HLE. The run still logs happily for its whole timeout, so the only
tell is zero flips.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-22 16:10:52 -06:00
Henrik RydgårdandClaude Fable 5.1 41627cc0ce docs: describe how the display list queue behaves
What SIGNAL and FINISH interrupts do and in which order, what enqueueing
checks, the pause window, what the sync calls wait for, and how
ProcessDLQueue() keeps the order right while executing lists in one go.

Also a note on gentest.py keeping a pipe open through usbhostfs_pc.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-21 12:21:19 -06:00
Henrik Rydgård 4d259a6f95 De-claude the comments. 2026-09-20 16:39:35 -06:00
Henrik RydgårdandClaude Opus 5 d40953d09b Document the headless traps that produce convincing but bogus measurements
Running a commercial game through headless to measure something has four ways to
report a clean pass for a run that proved nothing, and they compound: --log is
needed before anything is printed (and it goes to stderr, so grepping forces the
streams together), the memory stick defaults to one beside the exe rather than
the app's, which means no installed firmware and a silent LLE-to-HLE fallback,
an unrecognised parameter then hides in the merged output, and a count of zero
errors reads identically whether the code ran or never got there.

All four of these cost a round of wrong results while investigating ME memory.

Also records that the line-ending check has to be done on the bytes, since Git
Bash's grep normalises them: the obvious `grep -c $'\r$'` for a stray LF reports
every file clean, which is how a handful of LF lines got into two CRLF files
here without the usual whole-file-rewrite tell in git diff --stat.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-20 16:39:35 -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 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 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 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 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 d6c0a7bbde Firmware install: Change dialog layout, remove warning when not needed 2026-09-10 15:52:53 -06:00
Henrik RydgårdandClaude Opus 5 a3ce735123 AGENTS.md: spell out that commit messages carry no session marker
The rule was already here in one line and I still added a Claude-Session
trailer to ten commits, because the session's own attribution instructions say
to. Say plainly that this rule wins, and to check git log afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 12:52:19 -06:00
Henrik RydgårdandClaude Opus 5 fa577eaa83 Document --re-module in AGENTS.md and docs/reverse-engineering.md
A tool nobody knows about is a tool nobody uses. AGENTS.md gets a short section
pointing at it, plus the two things most likely to be got wrong when reading
the output: that a function's arity can't be inferred from the registers it
reads, since MIPS code passes arguments through untouched, and that a finding
is worth much more when the comment says which module it came from.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-08 15:32:23 -06:00
Henrik Rydgård 4d795f5130 Merge pull request #22248 from hrydgard/fat-short-names
sceIo: generate and resolve FAT 8.3 short names
2026-09-08 14:42:18 -06:00
Henrik Rydgård 87bb9dd965 docs: how to write a pspautotest and run it on a real PSP
We had a doc for running the existing tests against headless, but nothing on
the other half - bringing up PSPLink and usbhostfs_pc, what gentest.py does,
and how to get an .expected out of real hardware. Write that down, including
the parts that cost time to rediscover: usbhostfs_pc's working directory is
host0:/ so it has to start in the pspautotests root, gentest.py makes the
whole test directory and several old tests no longer build under pspdev's
GCC 15 (use -k), rebuilding a .prx with a newer toolchain balloons it, and
host0: is not FAT so anything testing FAT semantics needs ms0:.

Also adds the io/shortname test the doc uses as its worked example. It stays
in tests_next: hardware preserves the case of d_name where we uppercase it,
and appends ~1 to the short name of anything that isn't already valid
uppercase 8.3 where we only do that on a collision.

threads/tls/create moves to tests_next as well. It's collateral from the
submodule bump - upstream 1dcefeb regenerated its .expected on a PSP with
less free memory, so allocations at 1MB and above now expect failure, and
partitions 8 and 9 now expect 800200D2 where we return 800200D1.
2026-09-08 11:43:38 -06:00
Henrik RydgårdandClaude Opus 5 db839baa7e AGENTS.md: strengthen the HLE array-order rule, and stop claiming fixed line endings
The savestate rule about HLEFunction array order only lived under "Adding HLE
modules", so it read as advice for adding a module - not for adding one function
to an existing one, which is where it is easiest to get wrong. Promote it to Core
Safety Checks, where it applies unconditionally.

Also: whether a file is CRLF or LF depends on the checkout, since Windows auto-
converts everything to CRLF and Linux doesn't. Listing files as "CRLF" invited
converting them to match; the actual rule is to preserve what's on disk.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
2026-09-08 09:44:26 -06:00
Henrik RydgårdandClaude Opus 5 0e6614cf19 Report PSP file attributes rather than the host's
DirectoryFileSystem passed the host's permission bits and directory
size straight through to the game. The PSP has neither - its FAT driver
makes a mode up from the entry type and whether it's writable, and
reports no size for a directory. So a game saw 0644/0755 and a 4096
byte directory on Linux, but 0664/0777 and 0 on Windows, where the file
layer already synthesizes those bits.

Now both platforms report what the PSP does: 0777 for directories, 0664
for writable files, 0444 for read-only ones, and no size on a
directory. The parent ".." entry keeps its 4096, which is what the
hardware reports for that one.

Also fixes the synthetic PSP directory entry using 0x777 where 0777 was
meant.

Fixes io/directory/directory, moved to tests_good.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-05 09:58:10 -06:00
Henrik RydgårdandClaude Opus 5 3e87c79c89 AGENTS.md: Add a rule about branching off master before writing code
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01SGz54K3ZXa2Qzyc3aMEYyY
2026-09-04 12:18:46 -06:00
Henrik Rydgård 2820fa9467 Have Claude reorganize its own notes, as AGENTS.md was becoming very big. 2026-09-02 16:33:42 -06:00
Henrik Rydgård 3a6e6e3e0b Merge pull request #22197 from hrydgard/arm64-irjit-sxtw
Fix Arm64 IRJIT sxtw bug with kernel addresses
2026-09-02 21:51:11 +02:00
Henrik Rydgård ef6029cc2e STR_VIEW 2026-09-02 18:15:17 +02:00
Henrik Rydgård d9acafde85 AGENTS.md: spell out when a heredoc mangles a patch script
Git Bash strips one level of backslash escaping even with a quoted delimiter, so a
'\n' meant to land in the output arrives as '\n' and Python writes a real newline
instead. Records both failure signatures - a C2001 for the first, a silently
non-matching anchor for the second - and when to reach for Edit or a script file.

Delete file that shouldn't be there
2026-09-02 18:06:47 +02:00
Henrik RydgårdandClaude Opus 5 4b761f0715 Offer to install PSP firmware updaters instead of running them
Opening an official updater (a PSP/GAME/UPDATE EBOOT.PBP, identified by the
MSTKUPDATE disc ID) from the main screen now brings up a confirmation dialog
that unpacks the firmware into the NAND directory, where the emulated
flash0/flash1 live. Running the updater itself doesn't work, so there was
nothing useful to do with one before.

Unpacks the file list for the model we claim to be (iPSPModel), on a worker
thread, with a progress bar - for which PSARUnpackOptions gets an optional
progress callback.

AGENTS.md: translate UI strings last, in a separate commit

The English string is what all ~47 languages get derived from, so rewording it
after the sweep means redoing the sweep. Check the wording first.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-24 17:23:14 +02:00
Henrik RydgårdandClaude Opus 5 c9ff8f55fe langtool: Mark strings that are deliberately the same as the English one
Plenty of strings are legitimately identical in the target language - "Vsync",
"Status", "Ad Hoc multiplayer" - but the only test we had for "is this
translated" is "does it differ from English", so those got sent to the AI again
on every single run of finish-language-with-ai, costing tokens and giving it
another chance to translate something that shouldn't be.

They now get a "# same as English" comment and are skipped. finish-language-with-ai
writes it when the AI hands the English string back unchanged (which the
validation added earlier was throwing away as an error), and import-single writes
it when a language's line matches the en_US one. Deleting the comment puts the
key back in play.

Also stop comparing values with their comments still attached while working out
what's untranslated, which was leaking "# AI translated" into the context block
of the prompt.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 a1d205539c langtool: Let import-single create the key in en_US.ini too
It skipped the reference file, so adding a new string meant running
add-new-key-value first just to get the key into en_US.ini (and, as a side
effect, the English text into all 46 other files whether they were about to be
translated or not). Now an en_US line in the imported file is treated like any
other language, minus the "AI translated" comment, since that one is the string
the others were translated from rather than a translation.

So the workflow is one command plus validate. Languages left out of the import
simply don't get the key, and fall back to the English string at runtime -
copy-missing-lines is still there if you want the placeholder written out.

Instructions in AGENTS.md and the /add-string command updated to match.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 10bfda59ee Document the translation workflow, and add an /add-string command for it
Translating a UI string well means knowing what it does - what widget it is,
what the placeholders hold, how the neighbouring strings in that language are
phrased. langtool's AI commands can't know any of that, which is why their
prompt has a hand-maintained glossary that grows every time someone spots a bad
translation. An agent working in the repo can just go look.

So: AGENTS.md now describes doing the translating that way and letting langtool
do the file surgery (add-new-key-value, import-single, validate) instead of
hand-editing 47 files, and .claude/commands/add-string.md wraps it as a slash
command. Both say to skip a language rather than guess at it - the English
fallback is fine, a confident wrong translation nobody can proofread is not.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 afb2eb0623 AGENTS.md: document that new entries in an existing HLE module's function
table must go at the end too, not just new module registrations

Also note that a stale binary makes a bisect lie consistently

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-21 10:32:21 +02:00
Henrik RydgårdandClaude Opus 5 a58f54ba53 PSAR: let the caller pick which PSP model to unpack for
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-20 22:50:01 +02:00
Henrik RydgårdandClaude Opus 5 14b6623330 ImDebugger: Source column on the breakpoint list, and record learnings
Adds the source location column to the breakpoint list, the last of the places
worth surfacing line info. Exec breakpoints get a real location; memchecks and
register breakpoints get "-", since a watched data range and a register aren't
tied to a code address.

Fixed a pre-existing misalignment found while adding the column: the register
breakpoint row never emitted a cell for the Log column, so every following cell
sat one to the left - the register name appeared under "Type", the condition
under "OpCode" and the hit count under "Cond", with the last column left blank.
It has a Log checkbox now like the other two rows (register breakpoints do
support the log action), and the register name moved to Size/Label where the
memcheck row puts its size.

AGENTS.md gets what this stretch of work turned up:

- Most files here are CRLF, including every .vcxproj, Android.mk,
  Makefile.common and AGENTS.md itself. Patching one with a script that reads
  with universal newlines and writes with newline='' silently rewrites the whole
  file - it turned a two-line addition into a 5000-line diff, which is invisible
  in an editor and obvious in git diff --stat.
- Don't pipe Python containing backslashes through a bash heredoc; the quoting
  mangles them and anchors just fail to match for no visible reason.
- Headless registers its own debug-output listener, so exception and crash
  messages never reach the log - they go to stdout, block-buffered when
  redirected, and taskkill //F discards the buffer instead of flushing. Give the
  run a short --timeout and let it exit if you need to read a crash trace.
- 0xFFFFFFFF decodes to vflush, a real VFPU instruction, so it's useless as
  "garbage" for testing illegal-instruction handling.
- A wsdbg script has to stay connected long enough for what it asked for; ending
  with :quit straight after cpu.runUntilTime looks exactly like a broken feature.
- Where DWARF line info is and isn't available, since it's much narrower than it
  sounds.
- Corrected the note claiming broadcast.config.set only accepts logger and
  input; it takes all five categories now.

UnitTest 55/55.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 13:42:14 +02:00
Henrik RydgårdandClaude Opus 5 d72623c4a0 Load symbols from the unstripped ELF homebrew ships next to its EBOOT
Homebrew almost always ships the ELF it was built from alongside the EBOOT -
app.elf next to app.prx - but prxgen strips the symbol table on the way to the
PRX, so the module PPSSPP loads has no names at all and MIPSAnalyst calls every
function it finds z_un_<address>. Working out what any of them are meant hand-
parsing that ELF with a throwaway script, which is how the CrossCraft
relocation bug got identified.

So read it directly. On module load, scan the game's own directory for an ELF
with a symbol table and add its STT_FUNC/STT_OBJECT entries at the module's
base. CrossCraft picks up 3734 symbols, and the disassembly turns from
z_un_088c00f0 into world.init_empty, with static_allocator.alloc at the vtable
entry it calls - the two functions that took the longest to identify by hand.

Matching is the part worth getting right, since a wrong match puts confident
nonsense at real addresses, which beats having no names only in the sense that
it's worse. A candidate has to be a 32-bit ELF with a symbol table whose
highest section ends within a page of the loaded module's size - the companion
links at base 0 and covers the same image, so that's a tight check, and
unrelated ELFs sitting in the same folder fail it. Symbols outside the module
are skipped individually too.

Names go in with updateName, so they win over the analyzer's placeholders
rather than losing to whichever got there first. Gated on the existing
bAutoSaveLoadSymbols setting (off by default), which already means "keep symbol
names around for me" and avoids a directory scan per module load otherwise.

pspautotests 314/314.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 09:32:04 +02:00
Henrik RydgårdandClaude Opus 5 ef943ba205 Add --memstick, so headless can use a real memory stick directory
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 09:32:04 +02:00
Henrik Rydgård 68085804be Merge pull request #22093 from hrydgard/debugger-work
More websocket debugger features, per-module symbol maps
2026-08-17 23:20:11 +02:00
Henrik RydgårdandClaude Opus 5 59eb8a9a81 Report emulated time in cpu.status, and note headless/wsdbg traps in AGENTS.md
cpu.status only reported raw CPU ticks, which a client can't turn into a time:
the PSP's clock frequency is changeable and games do change it, so the
ticks-per-second ratio isn't fixed over a run. CrossCraft Classic runs at
333MHz, so assuming the default 222MHz reads 9.1s where the truth is 6.3s -
enough to put scripted input injection in the wrong place entirely.

Adds "us" (emulated microseconds) and "clockHz" alongside "ticks".
CoreTiming::GetGlobalTimeUs() can't be used directly for this: it rebases its
own internal counters as a side effect, and cpu.status is deliberately served
straight from the WebSocket thread rather than queued to the CPU thread (it's
meant to be cheap and frequently pollable). So PeekGlobalTimeUs() computes the
same value without the rebasing.

AGENTS.md picks up the things that cost time while driving headless over the
websocket API: --sync silently desynchronises on raw JSON lines because only
wsdbg's key=value shorthand gets a ticket; headless reports HAS_DEBUGGER as
false so anything gated on it silently does nothing there; the memstick is
hardcoded next to the executable; a leftover headless process turns a build
into an LNK1168 that looks like a compile error; wrapping the launcher in
`timeout` kills the emulator along with it, losing the crash you stopped at;
response field names aren't uniform (value vs uintValue); broadcast.config.set
rejects two of the four keys the docs list.

Also writes down the ELF-as-oracle technique that cracked the relocation bug -
when homebrew ships app.elf next to app.prx, the pre-link ELF still has the
symbols and the relocation symbol indices the PRX format discards, so the
loader's output can be checked exhaustively offline instead of by re-running.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 18:51:54 +02:00
Henrik RydgårdandClaude Opus 5 879baa6412 Add unit tests for BlockAllocator
BlockAllocator backs sceKernelAllocPartitionMemory and the various partition
allocators, and had no coverage at all. It's pure address bookkeeping - no real
memory - so it can be checked hard: after any sequence of operations the blocks
must still tile the range exactly, with no gaps, overlaps or strays, and
GetTotalFreeBytes/GetLargestFreeBlockSize must agree with what's really in the
list. ValidateAllocator() rebuilds the list through the public accessors and
asserts all of that, and it runs after every step of the randomised sections.

Covers bottom-up and top-down allocation, grain rounding, rejection of zero and
oversized requests, splitting and re-merging on free, double free and free of an
address that was never allocated, AllocAt including unaligned positions and
collisions, AllocAligned with alignments coarser than the allocator's grain,
filling the range completely and draining it, a range whose size isn't a
multiple of the grain, and two randomised churn loops - one plain alloc/free,
one mixing in aligned allocations and AllocAt to reach block layouts the simple
loop never produces. Fixed seeds, so a failure reproduces.

No bugs found - the allocator holds up. Verified the tests aren't vacuous by
injecting two plausible bugs: dropping the forward merge in MergeFreeBlocks, and
an off-by-one in the bottom-up fit check. Both are caught, the second pinpointed
to a specific churn iteration.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 14:50:51 +02:00
Henrik RydgårdandClaude Opus 5 5cfc4e4dae AGENTS.md: note the stale-incremental-build trap after a stash cycle
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 14:11:59 +02:00
Henrik RydgårdandClaude Opus 5 75ff0d406c Rename Memory::Lock() to Core_LockAgainstShutdown(), move it to Core
It stopped being about memory when CPU_Shutdown started holding it across the
whole teardown - it's what keeps kernel objects, the symbol map and the memory
map from being freed while another thread reads them. The old name invited the
reading that it locks memory *access*, which it has never done.

Memory::Reinit() now holds it across both halves rather than relying on
Memory::Shutdown()'s own acquire: between Shutdown() and Init() there is no
memory map at all, and a reader could slip into that gap.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 13:11:16 +02:00
Henrik RydgårdandClaude Opus 5 933d39406a AGENTS.md: document the debugger threading invariant now that lifecycleLock is gone
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 13:11:16 +02:00
Henrik RydgårdandClaude Opus 5 707275b46a Fix deadlock when stopping a game with a debugger request in flight
The WebSocket thread holds lifecycleLock across a whole event handler, and
handlers do their real work through Core_RunOnCPUThread(), which blocks until
the CPU thread drains the queue. Meanwhile PSP_Shutdown() ->
Core_NotifyLifecycle(STOPPING) took that same lock on the CPU thread. So the
debugger thread waited for the CPU thread while the CPU thread waited for the
lock the debugger thread was holding, and neither ever moved.

Drain the CPU queue while waiting for the lock instead of blocking on it. Core
state is still alive at STOPPING (it's notified before CPU_Shutdown), so running
those queued callbacks then is safe, and it lets the debugger thread finish and
release the lock.

Verified with a temporary instrumented build - a 3s sleep inside a handler while
holding lifecycleLock, arranged to overlap the game's shutdown - which hangs
reliably on the old code and exits cleanly with this change.

lifecycleLock stays for now: roughly half the subscribers and all the
broadcasters still read core state directly on the WebSocket thread instead of
going through the queue, and this is what keeps that from racing with teardown.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 00:34:01 +02:00
Henrik Rydgård 8784b709e8 Claude findings 2026-08-17 00:29:42 +02:00
Henrik RydgårdandClaude Sonnet 5 6eb204d882 unittest: support running multiple named tests in one invocation
main() only ever read argv[1], silently ignoring any further test
names despite both the usage text and the file's own top-of-file
comment claiming "one or more of the below" was supported. Rewrote
argument handling to collect every named test (or expand "all"),
validate all names up front (bailing with the usage listing if any is
unrecognized, rather than silently running a partial set), and run
them with the same pass/fail summary "all" already used - single-test
invocations now also get the "**** Running test X ****" banner "all"
always had, for consistency.

Verified: `UnitTest.exe CLZ MathUtil Path` now runs all three (was:
silently only CLZ); a bad name reports "Unknown test: X" plus usage
and exits 1; `all` and single-name behavior otherwise unchanged.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-13 08:09:42 +02:00
Henrik RydgårdandClaude Sonnet 5 27c1aa85ef Register InterpreterDispatch.cpp/.h in the UWP project; update AGENTS.md
Core/MIPS/InterpreterDispatch.cpp landed without being added to
UWP/CoreUWP/CoreUWP.vcxproj / .vcxproj.filters, breaking the UWP build
(CMake and the main Core.vcxproj build both succeed silently, so this
only surfaces when someone actually builds the UWP project). Verified
the fix by building CoreUWP directly via UWP/PPSSPP_UWP.sln.

Also updates AGENTS.md's "new .cpp/.c file" checklist from five places
to seven (adding the two UWP project files), documents that the UWP
build can actually be build-tested here via MSBuild rather than just
eyeballed, and notes this exact miss so it's easier to catch next time.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019SKhm9wKEQzRUsx9mTrrtQ
2026-08-12 13:06:52 +02:00
Henrik Rydgård 246ebd4c36 Frametests: resolve headless path to absolute, handle launch failures
The found binary path could be relative (e.g. 'PPSSPPHeadless' from the
repo root in CI), but tests run with the output dir as cwd, so launching
failed with FileNotFoundError. Resolve the path against the script's cwd,
and turn launch failures into per-test ERROR results instead of crashing
the whole run.

AGENTS.md: never push without asking first.
2026-08-09 21:37:36 +02:00
Henrik Rydgård 893ac3e3db Frametests: add framedump rendering test runner and PNG screenshot support
- 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.
2026-08-09 13:41:39 +02:00
Henrik Rydgård 8cd7e1b2c0 Delete all support for Qt
Our Qt backend has long been left behind and doesn't even support Vulkan
currently. There would be a lot of work to make it viable, and I don't
think anyone is really interested.

ImGui on SDL will soon fulfill the need for a more classic user interface
with a menu bar on Linux, and on Mac we already have a native UI.
2026-08-08 18:18:28 +02:00
Henrik Rydgård 9ad1cdaf38 Update AGENTS.md: BreakpointManager/SymbolMap locks removed, queue-drain fix 2026-08-08 17:22:12 +02:00
Henrik Rydgård a39efbdaec Document the debugger threading model in AGENTS.md 2026-08-08 17:22:12 +02:00
Henrik Rydgård bb3862b37e Revert "Extend LoadExecForKernel with real VSH loadexec/exit syscalls"
This reverts commit 510cfb421c.
2026-08-07 16:01:27 +02:00