Commit Graph
192 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5 b42a03e095 Add savestate serializer tests, fix three bounds-check bugs
PointerWrap and the Do() overloads around it are how every savestate is
written and read, and had no direct coverage. Everything read back came off
disk, so the corrupt-input paths matter as much as the round trips.

Three bugs, all in the bounds checking added in 58d4759ceb:

1. sizeof(T) is not a lower bound on how many bytes an element serializes to.
   It only holds for the types DoHelper_ writes out raw. A std::string is 32-40
   bytes in memory and serializes to as few as five; a T* serializes to whatever
   T::DoState() writes. So DoVector/DoList/DoSet/DoMap could reject a perfectly
   valid savestate whenever count * sizeof(element) exceeded the bytes left in
   the buffer. That is not hypothetical: pspFileSystem is serialized dead last
   in SaveStart::DoState, and MetaFileSystem::DoState does Do(p, currentDir) on
   a std::map<int, std::string>, so the check runs with only a few hundred bytes
   remaining and claims 44 bytes per entry against roughly 22 actual. Added
   SerializeMinElemSize<T>(), mirroring DoHelper_'s own condition, and used it
   in all five containers. The bound is only loosened, so nothing that loaded
   before can stop loading.

2. Do(p, std::map<K, T *> &) deletes every value before reading the new ones,
   and DoMap then returned on a bad count without clearing - leaving the map
   full of freed pointers to be used or deleted again. Six live maps go through
   this (sceMpeg, sceMp3, sceAac, sceFont, sceHeap, sceKernelThread's pending
   calls), so a corrupt savestate meant a use-after-free. Clear before the guard
   can bail out, in DoMap, DoMultimap and DoSet.

3. The wstring and u16string overloads validated stringLen < 0 but not 0, and
   didn't require a whole number of characters. read() computes
   stringLen / sizeof(char) - 1, so a length of 0 resized to SIZE_MAX and
   memcpy'd with a wrapped-around size. PSPOskDialog::DoState serializes both
   (inputChars at v2, a legacy wstring below that), so this was reachable: the
   test aborts the process without the fix.

The test covers round trips of PODs, strings (empty, embedded NUL), vector,
map, set, list and map-of-pointers, section titles and version gating in both
directions, marker mismatches, measure-vs-write checkpoint disagreement, the
error latch dropping to MODE_NOOP, every truncation of a valid buffer, and
hand-corrupted counts and lengths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 14:50:57 +02:00
Henrik RydgårdandClaude Opus 5 8265044a79 Add Hashmaps unit tests, stop tombstones from filling the table
DenseHashMap and PrehashMap are the open-addressed, linear-probing maps behind
the texture cache, the shader managers and the software renderer's
sampler/drawpixel caches, and had no coverage.

Writing the tests turned up a latent hang. Removal leaves tombstones, which
occupy probe slots exactly like live entries, but the load factor check only
looked at count_. So a workload that inserts and removes distinct keys keeps
count_ low forever while REMOVED fills the table, and no Grow is ever triggered.
Once there is no FREE bucket left, a lookup for a missing key has nothing to
terminate on - and the probe loops don't break out after their "Hit full"
assert, which is compiled out in release builds. The test reproduced it as a
hard hang in about a second.

Two fixes: count tombstones towards the load factor (rebuilding in place when
the load is mostly tombstones, growing otherwise), and make the probe loops
return instead of spinning if they ever do wrap all the way around.

Not reachable today - nothing in GPU/ calls Remove() on these maps, and
Maintain(), which exists to rebuild when tombstones pile up, is never called
anywhere. But Remove() is public API and the first caller to use it in a loop
would have hit an unexplained freeze.

Tests cover insert/get/miss/remove/size, tombstones not cutting a probe chain,
Iterate visiting exactly the live entries, Clear, growth past the initial
capacity, Rebuild compacting, a 20000-operation differential test against
std::unordered_map, and the tombstone churn above. PrehashMap gets the same
treatment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 14:50:57 +02:00
Henrik RydgårdandClaude Opus 5 ee8aaff6d2 Add SymbolMap unit tests, fix absolute symbols vanishing with no module loaded
SymbolMap had no coverage. It stores symbols relative to a module so they
survive that module being unloaded and reloaded elsewhere, and only symbols
belonging to a loaded module count as active - that indirection is where the
surprises are, so the tests concentrate on module lifetime and the shared label
table.

Writing them turned up a real bug. UpdateActiveSymbols() bailed out early when
activeModuleEnds was empty, as a "tiny optimization" for startup and shutdown,
having already cleared the active maps. But symbols with module index 0 are
absolute by design - they belong to no module, which is how you label a heap or
stack address - and the loops it skipped are exactly what keeps those alive.
So an absolute symbol disappeared as soon as the last module was unloaded, and
didn't exist at all before the first one was loaded. Dropping activeModuleEnds
from the early-out condition fixes it; the symbol-count half still gives the
intended fast path when there's nothing to do.

Tests cover function and data lookup by containing address, SetFunctionSize,
RemoveFunction/RemoveData, symbols surviving an unload/reload at a different
address, absolute (module 0) symbols, GetSymbolInfo/GetDescription, and Clear.
Two of them pin down behaviour that catches people out rather than asserting
it's right: AddLabel deliberately won't overwrite an existing label, and because
functions and data share one label table, renaming or removing via one affects
the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 14:50:57 +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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 14:50:51 +02:00
Henrik RydgårdandClaude Opus 5 a96dfc1390 Share SetupCRT with the unit tests so they can't pop a modal dialog
UnitTest.exe runs on CI and from tooling, where an assert or an abort() puts
up a message box that nothing will ever click, and the run just hangs until it
is killed. Headless already solved this; move its SetupCRT() into Common
(ExceptionHandlerSetup, which is where the rest of the process-level fault
setup lives) and call it from the unit tests too.

No behaviour change for headless. The OS-level SetErrorMode() call is now
guarded for UWP, which doesn't have it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 13:11:16 +02:00
Henrik RydgårdandClaude Opus 5 35a91b757a Move the temporary breakpoint out of the user's breakpoint list
step-over, step-out and run-until plant a one-shot breakpoint at the address
they want execution to return to. Keeping it in breakPoints_ alongside the
user's own meant the two kept colliding:

- Adding a log-only user breakpoint at the same address hijacked the temporary
  one. AddBreakPoint() didn't match across temp-ness so both existed, and then
  ChangeBreakPoint() looked up "the first enabled breakpoint at this address" -
  a log-only breakpoint isn't enabled, so the temporary one won and had its
  action overwritten to log-only. It lost PAUSE and the step never came back.
- RemoveBreakPoint() erased up to two entries per address to catch an
  overlapping temporary one, so deleting either deleted both - including the
  interpreter's cleanup path in CheckExecBreakpoints() taking the user's
  breakpoint with it.
- ExecBreakPoint() handled one breakpoint per address, so with both at the same
  address only one of them did anything: the step completed but the user's log
  line never printed.
- Nothing dropped it when something *else* stopped us first, so an interrupted
  step left a breakpoint armed at an address nobody was waiting for anymore,
  which later fired as a phantom stop.

It's a single TempBreakPoint member now, invisible to the breakpoint lists and
untouched by user edits. One is enough: step over/out and cross-thread step into
all require the CPU to already be stepping and resume it immediately, so only
one can be in flight, and run-until now replaces rather than stacking (two
pending run-untils had no coherent meaning, and the loser stayed armed).

Behavior follows what other debuggers do. Both breakpoints at an address are
evaluated independently and their actions combine, so a log-only breakpoint
logs without stopping and still lets the step finish. Core_Break() drops the
temporary breakpoint on any stop, whatever the reason - the same way gdb deletes
its step-resume breakpoint and lldb discards the thread plan.

Two things to be careful of, both covered by the new TempBreakpoints test:
HasBreakPoints() has to account for it, or the interpreter's checked run loop
and the JIT skip breakpoint checking entirely and a step with no user
breakpoints set never returns; and IsAddressBreakPoint() (user-facing, for the
lists and disassembly markers) is now separate from NeedsBreakCheckAt() (what
the JIT frontends and interpreter ask), since only the latter should see it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 00:29:26 +02:00
Henrik RydgårdandClaude Opus 5 0c510ca62e Add BreakpointManager::ChangeBreakPointAddress, use it from the ImDebugger
ChangeBreakPointAddress() moves the breakpoint keeping its action, condition and
log format, invalidates both ends, refuses to land on an existing breakpoint,
and resets the hit count since it belonged to the old address. The edit form now
works on a copy of the address and commits on deactivation rather than per
keystroke, so typing one address doesn't churn through every prefix of it.

The breakpoint edit form assigned straight to bp.addr and then invalidated the
icache at "bp.addr - 4, 8" - which by then is the *new* address - need both.

Also clear the selection after Delete in both edit forms - the reference into
the vector is dangling from that point on. Harmless today, but only because
nothing happens to touch it below.

Covered by a new Breakpoints unit test (verified to fail without the duplicate
check and the hit reset).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 00:28:39 +02:00
Henrik RydgårdandClaude Opus 5 6a05ef290d Fix four WebSocket debugger bugs found while stress testing
memory.readString could kill the connection: it copied raw emulated memory
straight into a JSON string, so any address not holding valid UTF-8 produced an
invalid WebSocket text frame.

hle.data.remove wiped the name of a function sharing the address. Labels are
shared between data and function symbols, so removing the data label left the
function showing up in hle.func.list with an empty name.

hle.data.add silently did nothing outside a loaded module. GetModuleIndex()
returns -1 for e.g. a heap or stack address, and symbols under that index never
reach the active maps - so the add reported success while the symbol was
invisible to list, and rename/remove then failed with "No data symbol found".
Falls back to module index 0 ("no module, absolute address"), which is the right
answer for a label the user put somewhere after a memory.search.

hle.thread.list reported the thread's stack base address in a field called
initialStackSize. Renamed to initialStack, matching the SceKernelThreadInfo
field it comes from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 00:25:39 +02:00
Henrik Rydgård 9b9083d3e5 Fix for headless port problem by Claude 2026-08-16 23:09:12 +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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-13 08:09:42 +02:00
Henrik Rydgård a44c2c0bde Replace System_SendDebugOutput with a registered callback
I normally try to avoid registrations when not needed, but in this case
only headless uses this, so it's motivated.
2026-08-11 22:36:47 +02:00
Henrik Rydgård 6f7de71ed0 Fix bugs in some UTF8 string handling utility functions 2026-08-07 11:56:14 +02:00
Henrik Rydgård d87a98c4a8 Avoid wasting CPU on counting characters in the memblock management
Unittest and a bugfix by claude.
2026-08-07 08:56:41 +02:00
Henrik RydgårdandClaude Sonnet 5 5bd1ff8066 Move remaining manual command line parsing into CmdLine.cpp
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
2026-08-05 16:02:49 +02:00
Henrik Rydgård de7989ae56 Add TextureReplacer unit test; make replacer testable standalone
Add unittest/TestTextureReplacer which creates a fictive texture pack
(textures.ini with readable invented hashes plus real PNG files), loads
it via the replacer, and verifies lookups, filtering, hashranges, mip
levels, and missing/ignored entries.

To make the replacer runnable outside the emulator:
- The constructor now accepts a null DrawContext (formats just default
  to unsupported).
- FindReplacement/FindFiltering use the replaceEnabled_ member instead
  of the global config.
- Added TextureReplacer::LoadPackForTesting() to load an ini from a
  path directly.
2026-08-03 18:55:33 +02:00
Henrik Rydgård b7b96c3374 Fix LZRC decompressor heap overflow and add unit test
The LZRC decompressor's only bounds check for output (and input) was a
debug-only _dbg_assert_msg_, which is a no-op in release builds. The
NPDRM demo block device also passed a hardcoded 1 MiB output length
while the real destination buffer (blockBuf_) could be as small as 2048
bytes, allowing a crafted NPDRM image to trigger an unbounded heap
overflow during game load.

Changes:
- rc_putbyte/rc_getbyte now enforce real bounds and set an error flag
  instead of relying on debug asserts; decompression aborts with -1 on
  overflow or truncated input.
- normalize() reads via rc_getbyte so it stays in bounds.
- Plain-text path clamps the copy size to both the output buffer and the
  remaining input (and no longer interprets the size as signed).
- NPDRMDemoBlockDevice::ReadBlock passes blockSize_ (the real buffer
  size) instead of 0x00100000 to lzrc_decompress.
- Add unittest/TestLzrc (synthetic input, no test data files): checks the
  plain-text clamp, truncated input, and output overflow all fail safely.
- AGENTS.md: note to reuse existing format handlers/decompressors before
  writing new ones.
2026-07-31 20:54:13 +02:00
Henrik Rydgård f3d7d8bc0c Fix Zip Slip in zip extraction and add unit test
A crafted zip with a parent-directory ("..") entry name could escape the
destination directory during extraction, writing arbitrary files on the
host (e.g. into startup/autostart folders). ExtractZipContents built the
output path by concatenating the raw zip entry name onto the destination
with no traversal check.

Changes:
- Add HasParentDirComponent() utility in Core/Util/PathUtil and use it in
  GameManager::ExtractZipContents to reject entries with a ".." component.
  Guard both the directory-creation and file-writing passes.
- Expose ExtractZipContents as public for testing.
- Add unittest/TestZipSlip which crafts a zip with a "../evil.txt" entry
  and verifies it is not written outside the destination directory.
2026-07-31 20:35:12 +02:00
Henrik Rydgård afe4d5beaf Buildfixes, dependency shuffling to make things more logical 2026-07-24 14:43:34 +02:00
Henrik Rydgård 16c7ec390f Headless: Start taking logic out of the "HeadlessHost"
Also update some agent files
2026-07-21 11:01:48 +02:00
Henrik Rydgård 4b24c187f2 Move more command line parameters to the new parser 2026-07-20 17:44:19 +02:00
Henrik Rydgård bd25962cbe Convert more options to the new command line parser 2026-07-20 16:33:03 +02:00
Henrik Rydgård dca65d02ad Move the force_gl_version command line hack to the new parser
See #20687
2026-07-20 16:33:03 +02:00
Henrik Rydgård e6875fa339 More cmdline work. 2026-07-20 16:33:03 +02:00
Henrik Rydgård 5c340ccd09 Remove the concept of scaled/offset depth buffers. 2026-06-10 10:41:30 +02:00
Henrik Rydgård 48084e9c3e Cleanup, fix some oversights 2026-06-03 15:23:17 +02:00
Henrik Rydgård 5b03bdaa8e Unit test fix, make the unit test a lot less log-spammy by default 2026-06-02 11:38:21 +02:00
Henrik Rydgård 3fa37a2bc1 Fix unittest 2026-05-30 19:07:59 +02:00
Henrik Rydgård ae98055cec Delete a lot of legacy depth and viewport code, fix OpenGL 2026-05-30 19:07:58 +02:00
Henrik Rydgård d6d2c24e24 Fix the toggle touch controls input (properly this time) 2026-02-05 09:44:42 +01:00
Henrik Rydgård 31dc48c815 Add a unit test to mat4 x mat4 2026-01-31 13:11:02 +01:00
Henrik Rydgård cbcec3e27f Bugfix GetFriendlyPath, use in more places 2026-01-29 23:15:32 +01:00
Henrik Rydgård 5f7a937466 Rename ValidSize to ClampValidSizeAt 2025-12-30 20:31:07 +01:00
Henrik Rydgård f5a211884b Minor cleanups, comments
Comments and warnings
2025-12-30 20:31:06 +01:00
Henrik Rydgård 9abd8c21e1 Finish implementing the multipart protocol, now multi file uploads work without corruption. 2025-10-27 12:20:24 +01:00
Henrik Rydgård b472eb8b8b Remove a lot of unnecessary string constructors 2025-08-31 15:49:29 +02:00
Henrik Rydgård 380fa19d3c Another std::string_view change, buildfix 2025-08-31 15:24:33 +02:00
Lin Runze 1d910b81ff loongarch: Add emitter, JIT and disassembler support for LoongArch64 2025-07-12 03:21:23 +08:00
Henrik Rydgård 62ff5ca81d Unit test fix part #1: Get more information 2025-06-24 15:20:50 +02:00
Henrik Rydgård 7cc42112c1 Add quick risc-v workaround in Unittest.cpp 2025-03-06 14:58:38 +01:00
Henrik Rydgård d6d1b5bbdc Add mechanism to run any closure on the main thread 2025-03-03 13:59:39 +01:00
Henrik Rydgård 33c4516e72 Implement new volume conversion functions, add test 2025-02-11 18:34:37 -06:00
Henrik Rydgård cf90b24431 Add UI volume setting (affecting navigation sounds and preview audio). Convert alt speed volume into a 100-step percentage. 2025-02-11 17:42:09 -06:00
Henrik Rydgård 9b923b72c1 Refactor audio output, preparing for better volume control 2025-02-11 16:43:29 -06:00
Henrik Rydgård 8907a4c001 More fixes, improve unit test 2025-02-06 10:26:46 -06:00
Henrik Rydgård e3f29cccd4 Add more tests 2025-02-06 10:26:46 -06:00
Henrik Rydgård 02b2a33980 Add test for mat*vec too. 2025-02-06 10:26:46 -06:00
Henrik Rydgård 46a472181b CrossSIMD unit test: Add sanity checks for the matrix multiplications 2025-02-06 10:26:46 -06:00
Henrik Rydgård 9caf37d28f Turn off depth raster if SIMD not available 2025-01-28 10:54:43 +01:00
Henrik Rydgård 7ddd7024f4 Revert unintentional change. Warning fix 2024-12-31 02:17:33 +01:00
Henrik Rydgård c3ac798545 More crosssimd 2024-12-31 01:59:08 +01:00