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.
The ARM64 IR JIT crashed on any load/store to a constant address with the
top bit set and an offset too large for an immediate - the kernel RAM mirror
at 0x88000000, for instance.
PrepareSrc1Address is careful about this: a constant address like 0x89100010
arrives sign-extended as a negative int64_t, and the (imm & 0xC0000000) ==
0x80000000 check turns it back into the positive value it should be. But when
the offset doesn't fit an immediate we fall back to loading it into a register
and using the register-offset addressing mode, which only takes the W half of
that register plus an extend - and we asked for SXTW, undoing the fix and
pointing the access ~2GB below the memory view.
Sign extension is still right when the offset is genuinely negative, which
happens when the base is a pointerified register and the displacement is a
negative one from the MIPS instruction. So extend based on the sign of imm.
Found by the Jit unit test, which stores to 0x89100000 and segfaults in the
JIT_IR phase - only reproducible on an actual ARM64 CPU, which is why it never
showed up on CI. The RISC-V and LoongArch backends get this case right already.
Written as (2 || 4 || 8 || misaligned), so every 2/4/8-byte access got labeled
'(unaligned)' and a genuinely misaligned larger access never reached the struct
branch. Log text only.
InitMemorySizeForGame read all of disc0:/UMD_DATA.BIN into a vector and then copied
it into a string, with no size limit, from an image we don't control - and the
DISC_ID that gets us here is equally forgeable, it just has to match one of the 16
g_HDRemasters entries. A real UMD_DATA.BIN is a few dozen bytes; anything larger is
a mistake or an attack, so check the size before reading.
The POSIX path used mlock() as though it were a mutex. mlock only pins pages in
RAM - it provides no mutual exclusion at all, so the read-modify-write of the
cross-process instance counter was unsynchronized. Two instances launched at the
same moment could both come away with PPSSPP_ID == 1, at which point both pass
IsFirstInstance() and write ppsspp.ini over each other, and both compute the same
adhoc local IP. Take an advisory lock on the shm fd instead. (The Windows path was
already fine - it uses a named mutex.)
Also, next/total are uint8_t in a segment that outlives the processes using it, so
next climbs across runs and wraps. Landing on 0 is worse than it looks: it isn't a
valid instance id, IsFirstInstance() fails, and config saving is silently disabled
from then on. Skip past it on wrap.
System.cpp stamped BootState::Complete unconditionally after InitGPU(), overwriting
the Failed that InitGPU sets when GPU_Init() fails - after it has already run
CPU_Shutdown(). PSP_InitUpdate then took the success path on a core that no longer
existed, down to a null Memory::base, and the first guest access dereferenced it.
InitGPU now reports failure and both callers honor it. (The libretro path had the
same problem from the other direction: it calls InitGPU after the Failed check.)
HandleAssert called g_assertCancelCallback directly on the IDCANCEL path, without
the null check its own BreakIntoPSPDebugger() helper does - and EmuScreen clears the
callback when a game is unloaded. So any assert after returning to the menu turned
"Cancel: skip and break into PPSSPP debugger" into a null jump, from the one button
whose entire purpose is surviving the assert.
__CheatDoState registered the cheat event type when the savestate had no CwCheat
section, but never scheduled it. CoreTiming::DoState has already swapped in the
state's event queue by then, which doesn't contain one either - so loading an old
savestate silently killed cheats, and the enable/disable polling with them, for the
rest of the session.
Achievements::ChangeUMD set g_isIdentifying and returned without clearing it when
hashing failed, leaving IsBlockingExecution() true forever - EmuScreen stops running
the CPU and the game is frozen until restart. Reachable from a disc swap on any ISO
whose PARAM.SFO or EBOOT.BIN can't be read.
x64Analyzer routed opcode 0x88 into the write path but had no case for it, so it hit
the default, logged from inside the crash handler, and failed. 0x88 is exactly what
the x64 JIT emits for a guest sb, so MemFault could never skip or ignore a bad byte
store the way it can a word one. Handle the 8-bit forms, and drop the 0x8a/0x8b cases
in the read path that the same 0xF0 mask made unreachable. Covered by a new
CheckAnalyze case, which fails without this change.
314 pspautotests pass, all unit tests pass.
Init() only runs on game load and Shutdown() nulls lua_ back out, but ImDebugger::Frame
draws the Lua console outside its PSP_IsInited() block, and whether the console is open
is persisted config. So opening it, exiting to the menu, and typing anything other than
the built-in clear/help/history dereferenced null.
GetStringErrorMsg had the strerror_r result test backwards. The XSI variant returns
0 on success, so every successful lookup returned "Unknown error"; and under glibc
with _GNU_SOURCE the GNU variant is selected instead, which returns the message by
pointer and typically leaves the buffer untouched, so it returned an empty string.
Either way GetLastErrorMsg() was useless on Linux, Android and macOS. Pick the right
handling by overload resolution rather than guessing which signature we got.
KeyMap's "no gamepad button mapped to cancel" fallback pushed into confirmKeys
instead of cancelKeys - and pushed the confirm button. So unmapping cancel left no
gamepad way out of menus, and duplicated an entry in the confirm list.
ControlMapper::AddListener mutated listeners_ without taking mutex_, while
RemoveListener takes it and the input thread iterates the vector under it. Opening a
screen while an axis is moving could reallocate it mid-iteration. The comment about
piggybacking on a screenmanager mutex was stale - there isn't one.
Config's two std::stof calls on PostShaderSetting values ran on user-editable ini
text with no try/catch, so a malformed entry called std::terminate during startup
config load. Use the same checked sscanf that LoadGameConfig already uses.
(CmdLine.cpp and Compatibility.cpp have the same pattern; not touched here.)
The screenshot downscale path leaked its final buffer on every downscaled shot,
which savestate thumbnails hit on every save at 3x and above.
HandleUploadPost is registered unconditionally, so closing the Upload screen left an
unauthenticated file-write endpoint live for as long as anything else kept the server
up. Check the flag in the handler.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
Same root cause as the websocket spin this branch fixes, different symptom, so it
belongs with the InputSink EOF change rather than apart from it.
ReadBinaryUntilTerminator() returns 0 both when nothing has arrived yet and when
the peer is gone, and the multipart loop only exits on finding its terminator - so
a cancelled browser upload left a handler thread spinning forever. That also hangs
web server shutdown, since it joins its handler threads.
Now that InputSink reports EOF, ask it.
(Moved here from misc-correctness-fixes, which will be rebased on top of this.)
The positional serialization means swapping two Mount() calls breaks every old
savestate just as thoroughly as adding one does, so say so. Also note that the
mount order spans more than MountFileSystems() - the UMD mounts are added first,
by MountGameISO, before we get there.
The positional serialization means adding a mount silently invalidates every
existing savestate unless its prefix is added to the skip list, which is exactly
how flash1: broke them. Write down the rule, and note that renaming or removing
a mount isn't something this scheme can handle at all.
78ef1eae82 added a flash1: mount, but MetaFileSystem::DoState serializes the
mounts positionally - one section each, with no length to skip by - so a state
written before that commit has one section fewer than we now have mounts.
The existing count check assumed a single missing entry could only be pfat0:,
which was the previous mount added this way. So it took the skipPfat0 path,
skipped pfat0's section while still only looping n times, and ended up making
n-1 DoState calls against n sections. Everything after that read shifted, and
the load died with "Failure at DirectoryFileSystem".
Make the "these were added later" set explicit and ordered instead, and iterate
over the mounts rather than over the saved count, so the number of DoState calls
matches the state regardless of which of them are missing.
Verified against Wipeout Pure (UCUS98612): both save slots report n=9 against 10
mounts and fail to load before this, and load after, in both the app and headless.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
Memory::IsValidAddress and friends tested the extended-RAM range with
(address & 0x3F000000), i.e. at 16MB granularity, so they accepted the whole 16MB
block containing the end of RAM. That's harmless at 32MB and 64MB, but the Sora no
Kiseki SC/3rd HD remasters run with 0x04C00000, so addresses from 0x0CC00000 to
0x0CFFFFFF read as valid, and MaxSizeAtAddress then underflowed to ~4GB there -
which defeats ClampValidSizeAt and IsValidRange entirely for that window. Mask
with 0x3FFFFFFF instead, in all five helpers and the copies in MemMapFunctions.cpp.
IsValidTextureAddress's extended-RAM branch repeated the first branch's whole mask
rather than just its alignment bits, so it was dead code and extended RAM was never
accepted as a texture source.
ComputeTextureHash checked IsValidAddress(addr + sizeInRAM), i.e. only the end
address, which can land in a different valid region than the start - a VRAM texture
with a large enough computed size ends exactly at the base of RAM and "passes"
while reading far past the 8MB VRAM view. Use IsValidRange.
TextureReplacer::ComputeHash's strided path had no range check at all, unlike the
contiguous path right above it. Also clamp the pack-supplied reduce-hash factor to
1.0 - it's a reduction, and the ini parser only rejects exactly 0.
ZipExtractFileToMemory read an uninitialized zip_stat when zip_stat_index failed
(it ignored the return value) and sized a host allocation directly from the zip's
declared uncompressed size. Reached just by opening an archive.
Memory::Reinit ignored Init()'s return value, and DoState fed it a memory size
taken straight from the savestate. A bogus size made the map fail to allocate and
left base null, after which DoMemoryVoid wrote RAM through it. Validate the size,
propagate the failure, and roll back to the previous size if reinit fails.
314 pspautotests pass, all unit tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
GetCurrentDrawAsDebugVertices (GE debugger vertex preview) sized its index scratch
buffer at a fixed 65536 and then ran both expanding steps into it: index generation
turns strips/fans into up to 3 indices per input index, and RunSoftwareTransform can
then expand points/lines/rects into 6 more each. A 30000-vertex triangle strip wrote
~90000 entries. Size the buffer from the count instead.
The Expand{Rectangles,Lines,Points} capacity checks were also off: they compared the
expansion against indsSize but write the expanded indices at inds + vertexCount, so
the input count has to be part of the sum.
ControlMapper::Axis wrote rawAxisValue_[axis.axisId] with no bounds check, one line
below an explicit check on axis.deviceId. axisId comes straight from the device -
Android reports AXIS_GENERIC_13..16 as 44..47, against a 44-entry array - so it wrote
into the neighbouring deviceTimestamps_. NativeAxis had the same unchecked write into
HLEPlugins::PluginDataAxis, where it goes out of the object entirely.
Rewind's LockedDecompress computed its copy-from-base block size as
base.size() - result.size() in size_t and truncated to int, so it went negative once
the output grew past the base, and insert() then ran with last < first. That happens
because a state can outlive the base it was compressed against: there are 20 states
but only 2 bases, rotated every 16 saves. Track a generation per base and refuse to
decode a state whose base is gone, and bound the block size against the base itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
In LOCAL_FOLDER share mode, LocalFromRemotePath ended with
return Path(g_Config.sRemoteISOSharedDir) / decoded;
sRemoteISOSharedDir defaults to empty and nothing requires the user to pick a
folder before pressing "Share Games (Server)". Path::operator/ doesn't insert a
separator when the component already starts with one, so with an empty base it
returns the component verbatim - "GET /etc/passwd" resolved to Path("/etc/passwd"),
which is non-empty and went straight to DiscHandler. The backslash, "/.." and "//"
filters never fired, because no traversal is needed to get there. That is an
unauthenticated arbitrary file read for anything that can reach the port.
Refuse to resolve anything when no shared directory is configured, and check that
the joined path actually stays inside it. HandleListing needs the same guard: it
called GetFilesInDir on the empty path, which on Windows becomes
FindFirstFile("\*") - a listing of the root of the current drive.
Also log a warning when the server starts in this state, so "nothing is shared"
doesn't look like a mysterious failure.
The empty-base behavior of Path::operator/ is surprising enough to be worth
pinning down, so TestPath now asserts it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
VulkanGraphicsContext::InitSurface() threw away VulkanContext::InitSurface()'s
VkResult and carried on, so a failed surface init surfaced as
_dbg_assert_(GetAvailablePresentModes().size() > 0) in the VKContext
constructor rather than as a graphics error with the usual backend fallback.
The vkCreate*SurfaceKHR failure path in ReinitSurface() didn't log anything
either, so the assert was the only trace of it.
Now ReinitSurface() logs and sets init_error_ for all three ways it can bail
(surface creation, ChooseQueue, present mode enumeration), InitSurface()
checks the result, and MainThreadFunc() passes the message back out instead of
writing it to a local it then drops - Windows/main.cpp was reporting
"Failed to initialize main thread function." to the user.
Also deletes the Application on that failure path, which was leaked.
The two long-standing bug reports had a shared root: the service loop could
end up in a state it never left.
- Exit hang: UPNP_CMD_EXIT was queued alongside port requests and only acted
on when it reached the front. A request that couldn't complete was never
popped, so exit sat behind it forever and join() blocked indefinitely.
Exit is a flag now, checked before anything else.
- CPU spin: wait_for() with a predicate returns immediately when the predicate
already holds, so a stuck queue head meant a tight loop. sceNetInet's bind()
queues UPnP_Add regardless of the setting, so this hit whenever UPnP was off
and a game used sockets. The loop always blocks now, and requests are dropped
while UPnP is off.
- Failed discovery was retried every 5s forever, each time a full 2s SSDP round
plus an error toast. Now backs off 5s -> 300s and reports once.
Other things found while in here:
- Every failed Initialize() leaked a UPNPUrls + IGDdatas, so ~every 5 seconds
for anyone with UPnP on and no router. The manual miniwget/parserootdesc/
GetUPNPUrls block was also redundant - UPNP_GetValidIGD does all of it and
memsets over the result, leaking the URLs and costing an extra HTTP round
trip per attempt.
- UPNP_GetValidIGD's status was never checked, so we could go DONE with no
usable IGD and hand a NULL controlURL to UPNP_GetConnectionTypeInfo.
- miniupnpc's strncpy into the port-mapping-entry buffers doesn't guarantee a
terminator; an 80-char description ran std::string off the end of desc[80].
- Add() marked another app's port "taken" only after our own add succeeded, so
a failed add left their mapping deleted and never restored.
- Clear() walked the router's entire table at exit, one HTTP round trip per
index. It now deletes only what we know we mapped, and the exit cleanup has
a time budget so an unreachable router can't stall shutdown.
- The in-flight request stayed in the queue during the router call, so a
same-port request arriving concurrently could erase it and be dropped
unexecuted.
- The queue is bounded, and last-write-wins per port collapses the churn from
games that rebind in a loop.
- The mapping description is built when the request is queued rather than read
off g_paramSFO from the UPnP thread later.
The thread now only exists while the setting is on - turning it off makes it
remove its mappings and exit, turning it on starts one. That means __UPnPInit()
has to run after the config is actually loaded; g_Config.Init() only builds a
lookup table. QueueRequest() reconciles too, so a per-game config or a libretro
core option enabling UPnP works without a notify at every call site.
The settings checkbox is disabled in-game, since sceNet latches related
settings at boot and a game that already mapped its ports wouldn't cope with
them disappearing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
div.s now maintains fcr31's Cause.Z and (when the trap is masked) sticky
Flag.Z bits, in the standard MIPS bit positions. Only a finite non-zero
dividend counts, so 0/0, inf/0 and NaN operands are excluded per IEEE 754.
When the guest has the trap unmasked, the new Core_FPUException() reports it
with the usual module suffix and MIPS call stack, and fd is left unwritten as
hardware would. That's gated behind a new developer setting, off by default:
PSP threads start with fcr31 = 0x00000e00, i.e. three of the traps already
enabled, and games divide by zero without meaning anything by it. The fcr31
bits are updated either way, so what the game reads back doesn't depend on
the setting.
Interpreter only - the JITs are unchanged, and none of this is reachable
under them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9fKXvYBrnqtp1QQGaGWVv
Checked against two PSP binaries that shipped with intact symbol tables,
which turned up several constructs the format's usual description doesn't
mention:
- Template arguments are written literally inside the length-prefixed name
("39CList<Q38hlScreen5Brwsr13CContentsUnit>"), not with a "__PT" prefix,
and they nest. Function templates put theirs in the base name instead,
followed by the return type.
- A family of "@"-decorated symbols for things with no C++ name: thunks
("@12@__dt__3SonFv"), string literals, function-local statics and their
guard variables. Plus __vt__/__RTTI__/__sinit_, printed in the same style
as the Itanium special names.
- Types are now built as a split declarator, so a pointer to a function
comes out as "int (*)(int)" rather than "int (int) *".
Also stop the lenient pass from turning plain C names with a "__" in them
into nonsense - "I3dClut__FlushCache" became "I3dClut(long, ...)". It now
requires a class qualifier, which costs nothing: over ~10000 symbols the
lenient pass rescued none and only produced those false positives.
Symbol map names go from 128 to 256 characters, since a demangled name
keeps its parameters and templates make short work of 128.
docs/CodeWarriorMangling.md describes the format, marking the parts that
are inferred from cfront rather than attested in a real binary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY
Core_ProcessStepping() returns immediately when the CPU is stopped with nothing
queued, so Core_RunLoopUntil() returns immediately, so whatever drives it comes
straight back. headless does that in a loop with no frame pacing at all, so a
paused emulator sat at 100% of a core: measured 6.02 CPU-seconds over 6 wall
seconds parked at startBreak. A debugger session is stopped most of the time, so
this also dominated any profile taken of one - showing up as synchronization
overhead around Core_RunOnCPUThread, which was just the hottest thing inside the
spin rather than a problem with the queue.
The CPU thread now blocks on a condition variable in that case. Anything that
gives it something to do wakes it - Core_RunOnCPUThread() on push (with the
queue mutex held, so it can't sleep on a task already queued),
Core_RequestCPUStep(), and Core_Resume() - so the 2ms timeout is only a backstop
for state changed without a wake, never how work is normally noticed.
The wait is deliberately short rather than indefinite: callers do real work after
Core_RunLoopUntil() returns, and in the app build that includes rendering the
ImGui debugger from this same thread, so this has to bound how long a paused
frame takes rather than replace the frame loop.
Now 0.05 CPU-seconds over the same 6 seconds. No measurable cost to anything
else: an identical scripted boot runs in 2514ms vs 2476ms before, and 20
consecutive cpu.stepInto still complete promptly. 55 unit tests pass, 314/314
pspautotests with --graphics=software.
Also: wsdbg's README claimed a raw JSON line gets a ticket auto-assigned when it
lacks one. It doesn't - the code deliberately sends raw lines exactly as written,
and omitting the ticket is how you say "not waiting for an answer". Corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
These two raised the memory exception and then went ahead and did the access
anyway, unlike every other load/store here. A quadword access that isn't
16-byte aligned isn't valid, so there's nothing to carry out - and on 64-bit,
where GetPointerUnchecked is base + address with no masking, an address that
failed the validity check meant dereferencing whatever that landed on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
vrot clears the D prefix for the cosine lane, since the prefix doesn't apply
there, but shifted the saturation mask by cosineLane rather than cosineLane * 2.
That field is two bits per element - ApplyPrefixD reads it as (data >> (i * 2))
& 3, and every other site in the file shifts accordingly - so for lanes 1 and up
it cleared the wrong lane's saturation and left the cosine lane's in place. The
mask field next to it is one bit per element and was already right.
Only reachable through the interpreter, but that includes the JITs, which fall
back here for any prefixed vrot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
ins derived its width as (_SIZE + 1) - pos, which is zero or negative when the
encoded msb is below pos: the following shift is then 32 or more, undefined, and
on x86 produces an all-ones mask that writes bits the JITs don't touch. Build
the mask from msb and shift it down instead, which is what the JITs do and can't
shift out of range. Hardware calls that encoding unpredictable, so consistency
is all that's wanted here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
The alignment checks added in d8edeb7649 return out of the instruction handler
without advancing PC, so with IgnoreBadMemAccess - which is the default, and
which makes Core_MemoryException log and return - the run loop comes straight
back to the same instruction and never gets past it. cpu/crash/crash_read_u32
under -i logged the same SIGSEGV 585096 times in 30 seconds before being killed;
the JIT runs it to completion.
Continue instead, the way Memory::Read_U32 did before those checks existed and
the way the JIT's safe-memory path still does: loads produce zero, stores are
dropped, PC advances. When the exception is set to break rather than ignore,
Core_Break has already stopped the core by the time we get here, so nothing
changes for that case.
lv.q/sv.q are left alone - they already fall through and do the access.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
Every JIT backend puts the host FPU into the mode fcr31 asks for (bits 0-1 and
24) before running emulated code, and takes it back out before calling any host
code. The plain interpreter did none of that, so all its float math rounded to
nearest with denormals intact no matter what the game had set - cpu/fpu/fpu
fails under -i and passes under the JIT on exactly this.
Move the helpers the IR interpreter already had for this out of IRInterpreter
and into MIPS.cpp as ApplyHostRoundingMode/RestoreHostRoundingMode, and use them
around the interpreter's run loop and single step, restoring around syscalls and
replacement functions, which are host code. ctc1 re-applies immediately, since
the interpreter has no block boundary to defer it to.
round.w.s changes with it: it was floorf(x + 0.5f), which is half-away-from-zero
rather than the half-to-even every JIT produces, and the add would now pick up
the guest's rounding mode on top of that. round_ieee_754 is both correct and
mode-independent, and is what cvt.w.s already used for the same rounding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
The disasm window cached the flattened symbol list and only rebuilt it when one
of three menu items said so. Nothing marked it dirty when a game booted or
exited, and a new SymbolMap is allocated per boot, so the list kept showing the
previous game's functions.
Give SymbolMap a version counter that every mutator bumps, and let the window
compare against it instead. The counter is process-wide rather than per-map, so
a fresh map can't hand out a version a cached copy already holds.
Also re-find the selected symbol by address after a rebuild (the index means
something else afterwards), and drop the unused symbol cache members in
ImMemWindow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
C++ homebrew has an unreadable symbol table -
everything is _ZN10PxRenderer7DrawImmE... - which makes the disassembly and
symbol list nearly useless. Add an Itanium C++ ABI demangler and run ELF
symbols through it on load, in both ElfReader::LoadSymbols (unstripped EXECs,
which is what a CMake pspdev EBOOT actually contains) and the companion-ELF
path.
The demangling standard is called Itanium for historical reasons - it
was defined for Itanium but ended up being almost universally
applicable.
Written from scratch rather than using __cxa_demangle, which doesn't exist on
MSVC/UWP, or vendoring LLVM's demangler, whose license doesn't fit. Anything
unrecognized (arbitrary constant expressions, decltype) aborts the parse and
the caller gets the original mangled name back, so a caller never sees a
half-parsed result. Recursion is depth-capped since the input comes from a
file we didn't write.
Checked against c++filt as an oracle: of 1089 mangled symbols in a real C++
homebrew EBOOT, one differs; of 55189 from libstdc++/libLLVM/cc1plus, 22
differ and 413 are declined. Fuzzed with 220k mutated and random inputs under
ASan/UBSan.
Also adds a right-click menu to the ImDebugger symbol list.
Note that SymbolMap stores names in char[128], so the longest STL names get
truncated in the UI. Still far more readable than the mangled form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3DbkJ8ShYiXU7q5Tv1LZu
When launching a file from outside the main screen (file association, shortcut,
drag-and-drop), the info hasn't been computed yet, so the file type and ID checks
that decide what to do with the file were reading empty data. Add
GameInfo::WaitUntilReady() - a condition variable signalled from
MarkReadyNoLock(), which every exit path of the work item goes through - and use
it there.
Also demote a noisy PRX decryption log line to DEBUG.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMLTdwyyzU6Mze3w8VC2JL