Commit Graph
47339 Commits
Author SHA1 Message Date
Henrik Rydgård 85efe6ba7c Instance: lock the shared counter for real, and don't hand out ID 0
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.
2026-08-31 12:53:34 +02:00
Henrik Rydgård f094752f21 MemoryUtil: fall back to far memory when the near reservation fails
The x86-64 path searched for free memory near the code, and if it found some,
committed to it - if that VirtualAlloc failed, ptr was left null and we returned
null, never reaching the else branch that exists precisely to say "can still run,
thanks to RipAccessible".

Finding a free region isn't the same as being able to reserve it. VirtualAlloc
rounds a non-null lpAddress down to the 64K allocation granularity while
SearchForFreeMem only guarantees page alignment, so the rounded-down base can land
back inside a committed region; a concurrent allocation between the VirtualQuery and
the VirtualAlloc does it too. Callers don't check the result - AllocCodeSpace stores
it unchecked and the emitters write from there - so this turned into a wild write
rather than a clean JIT-unavailable fallback.
2026-08-31 12:52:35 +02:00
Henrik Rydgård cf8ef68f6b Merge pull request #22182 from hrydgard/remote-iso-empty-share-dir
Remote ISO: don't serve the whole filesystem when no folder is set
2026-08-31 12:47:23 +02:00
Henrik Rydgård 10df1ee5fb Merge pull request #22181 from hrydgard/android-fixes
Android: Remove unused code, bump gradle. InstallUpdateScreen now also allows Run
2026-08-31 12:42:14 +02:00
Henrik Rydgård 25fd396138 Merge pull request #22183 from hrydgard/net-thread-tracking
Net: reap finished connection threads, and don't echo unsendable close codes
2026-08-31 12:41:55 +02:00
Henrik Rydgård 9b896e0f7d Merge pull request #22173 from hrydgard/range-validation-fixes
Claude code review: Fix a batch of missing or wrong range validation
2026-08-31 12:32:26 +02:00
Henrik Rydgård 57d22d01d1 Net: reap finished connection threads, and don't echo unsendable close codes
NewThreadExecutor::Run pushed a std::thread per connection and only ever joined them
in the destructor, so a server leaked a joinable thread object for every connection
it had ever served. Measured with 60 connect/disconnect cycles against the debugger:
handle count +60 before, +1 after. Each worker now flags itself done as its last act,
and Run() reaps the finished ones first. Only the accept thread calls Run(), so the
flag is the only thing that needs to be atomic.

Note this doesn't bound how many connections can be in flight at once - it just stops
the finished ones from piling up.

Separately, a received close code was echoed straight back. RFC 6455 7.4.1 reserves
1004, 1005, 1006 and 1015 for describing how a connection ended locally, so they must
never go on the wire - echoing one back would be our protocol violation rather than
the client's. Send PROTOCOL_ERROR when they give us something we can't repeat.
2026-08-31 11:45:53 +02:00
Henrik Rydgård 3bd9e23f91 Merge pull request #22174 from hrydgard/misc-correctness-fixes
Claude code review: Misc minor correctness fixes
2026-08-31 11:37:02 +02:00
Henrik Rydgård d828d0162c Merge pull request #22176 from hrydgard/metafilesystem-savestate-compat
Fix savestate breakage related to file system mounts
2026-08-31 11:35:22 +02:00
Henrik Rydgård 6a36ed7de9 Translation string for run command 2026-08-31 11:33:51 +02:00
Henrik Rydgård 6ef95ebff7 langtool: New command remove-ampersands 2026-08-31 11:31:35 +02:00
Henrik Rydgård 04a6166c90 Android: Remove some unused code 2026-08-31 11:02:32 +02:00
Henrik Rydgård e21eb294d0 InstallUpdateScreen: Also allow trying to run, for development purposes 2026-08-31 11:02:32 +02:00
Henrik Rydgård d2207e81d7 Another gradle bump 2026-08-31 11:02:32 +02:00
Henrik Rydgård 7538e1a3df Merge pull request #22179 from hrydgard/websocket-disconnect-eof-handling
Claude code review: Websocket disconnect EOF handling
2026-08-31 11:02:20 +02:00
Henrik Rydgård 27cfc4ebe8 Merge pull request #22172 from hrydgard/emitter-encoding-fixes
Claude code review: Emitters: fix a batch of encoding bugs
2026-08-31 10:52:06 +02:00
Henrik Rydgård 6b0e2b281b Merge pull request #22175 from hrydgard/depthraster-overflow-fixes
Claude Code Review: DepthRaster: fix host buffer overflows in the depth raster queue
2026-08-31 10:44:28 +02:00
Henrik Rydgård f4b0c604cd Merge pull request #22178 from saboten731/pr/libretro-vulkan-presentation-handoff
Libretro Vulkan: wait for presentation handoff completion
2026-08-31 09:54:58 +02:00
keycross c40790a476 Libretro Vulkan: wait for presentation handoff completion 2026-08-31 11:29:40 +09:00
Henrik Rydgård fb38b3eb66 Net: three smaller fixes from reviewing the server code
RequestHeader::GetParamValue indexed parts[1] without checking the size. A query
parameter with no '=' at all ("?foo") makes SplitString return a single element, so
both the DEBUG_LOG and the assignment read off the end of the vector. Nothing calls
GetParamValue today, so this is latent rather than live, but it's driven straight
off the request line.

The 64-bit frame length was assembled with header[n] << 24 on uint8_t values, which
promote to int - a byte >= 0x80 in the top position shifts into the sign bit and then
sign-extends when widened to uint64_t. The resulting size was always rejected, just
by the wrong check and via signed overflow to get there. Cast first.

OutputSink::Block() had the same shape as the InputSink one this branch already
fixed: a broken socket is reported ready immediately and forever, so waiting on it
is a spin. Bail if the sink already knows it's broken.
2026-08-31 01:24:56 +02:00
Henrik RydgårdandClaude Opus 5 375fa0fc11 Misc correctness fixes
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-31 01:19:06 +02:00
Henrik Rydgård 949f773220 WebServer: bail out of an upload whose peer disconnected
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.)
2026-08-31 01:02:46 +02:00
Henrik RydgårdandClaude Opus 5 abd91da5eb WebSocket: teach the input sink about EOF, and stop losing why a read failed
Follow-up to the previous commit, from Nemoumbra's questions - which found a worse
spin than the one that fix addressed.

InputSink couldn't tell "nothing right now" from "peer is gone": Fill() treats
recv() == 0 as no data and only sets hasError_ on a real error. Block() then waits
with WaitUntilReady(), which reports a closed socket as ready immediately and
forever, so TakeExact() looped on it without ever returning. A client that
disconnects with half a frame buffered - easy to do while blasting messages - put
the server in an infinite loop inside TakeExact, never even returning to Process().
Measured 7.95 CPU-seconds over 8 seconds; 0.08 after.

So: track EOF explicitly (sticky atEnd_, exposed as AtEnd()), and have Block() give
up when nothing more can arrive.

That information was being thrown away in three more places:

* Process() only tried to fill when the sink was already empty, so a disconnect went
  unnoticed for as long as there were leftovers - and if those leftovers were a
  partial frame, the read above never completed. Always fill, and close once the
  peer is gone and we've consumed what it sent.
* ReadPending() uses TakeAtMost(), which returns 0 both for "nothing right now" and
  "nothing ever again", and then reported success having consumed nothing. Ask the
  sink which it was.
* Both TakeExact() call sites answered a failed read with POLICY_VIOLATION, blaming
  the client for a protocol error when it had simply disconnected. Check the sink
  and report ABNORMAL when that's what happened.

Also stop queueing data once our own close frame is queued. RFC 6455 5.5.1 forbids
data frames after a close, and beyond the protocol, anything appended afterwards
keeps the buffers non-empty and starves the "everything is flushed" check that ends
the connection. Observed the server pumping 167MB of log broadcasts after being
asked to close.

The repeated close-and-discard is now one helper.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-31 00:57:17 +02:00
Henrik RydgårdandClaude Opus 5 03b313e22b WebSocket: don't spin at 100% CPU when the output can't drain
Reported by Nemoumbra: the debugger server could get stuck in a tight select()
loop after a lot of traffic, burning a core.

Once OutputSink hits a real send() error it latches hasError_, after which Flush()
returns immediately without consuming anything, so out_->Empty() is false forever.
Process() waited for that to empty before finishing the close, kept the fd in the
write set, and select() reports an errored socket as ready every time - so it
returned true on every lap without ever making progress, and WebSocketDebuggerLoop
span. This needs sentClose_ to be set for it to be unrecoverable, since otherwise
the read side notices the disconnect and closes; a client that sends CLOSE (or
trips a protocol error) while output is backed up gets exactly that. Reproduced
with a client that queues ~120MB of responses, sends CLOSE, then resets the
connection without reading: 6.02 CPU-seconds over 6 seconds before, 0.06 after.

Treat an output error as fatal to the connection instead.

Also, select() returning -1 always returned true, so any error that doesn't fix
itself (a bad fd rather than EINTR) was a second busy-loop with no wait at all.
EINTR retries, everything else closes.

Finally, SendFlush() erased the drained bytes off the front of outBuf_ every lap.
With a backlog that's a memmove of the whole buffer per lap, i.e. quadratic in the
backlog, which burns CPU on its own while draining a slow client. Track a consumed
offset and only compact once the dead prefix is worth reclaiming.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-31 00:42:43 +02:00
Henrik Rydgård bdce0e2e44 Buildfix: include <cfloat> for FLT_MAX in TestArm64Emitter
MSVC pulls it in transitively, gcc and clang don't - broke the gcc-normal,
clang-normal, macos and test-headless-alpine CI jobs.
2026-08-31 00:29:22 +02:00
Henrik Rydgård 1295512866 MetaFileSystem: also warn against reordering existing mounts
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.
2026-08-31 00:23:53 +02:00
Henrik Rydgård 7b409af50f MetaFileSystem: spell out how to add a mount without breaking savestates
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.
2026-08-30 23:59:06 +02:00
Henrik RydgårdandClaude Opus 5 8768c30b1d Headless: actually apply --state, and report the result
SaveState::Load only queues the operation - SaveState::Process() applies it, and
headless never called that anywhere. So --state silently did nothing: the state
was queued before boot and sat in the queue for the rest of the run. Call it at
the top of the run loop, the same place in the cycle EmuScreen::render does.

Also pass a callback so the outcome is visible, and make a failed load set a
non-zero exit code. Without that, headless reported success for a state it never
loaded - which is why it didn't catch the savestate regression this branch fixes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 23:54:29 +02:00
Henrik RydgårdandClaude Opus 5 729653bf42 Fix loading savestates made before flash1: was mounted
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 23:52:09 +02:00
Henrik RydgårdandClaude Opus 5 016f976b1f Fix a batch of missing or wrong range validation
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 23:05:04 +02:00
Henrik RydgårdandClaude Opus 5 7a13881245 DepthRaster: fix host buffer overflows in the depth raster queue
CalculateDepthDraw guarded depthVertexCount_ with vertexCount, which is the
index count - but depthVertexCount_ grows by the number of decoded vertices,
which for an indexed draw can be far larger. Two draws with 6 indices spanning
40000 vertices each therefore passed the check and wrote ~700KB past the end of
depthTransformed_. It also never bounded depthIndexCount_ against depthIndices_
at all, which only has room for 3 indices per vertex slot while draws routinely
produce more. Pass the decoded count in separately and check both.

DepthRasterClipIndexedTriangles duplicated culling-disabled triangles twice: once
in the collect loop (added for Syphon Filter, #21498) and again in the output
stage, which was the older code and should have been removed then. So it emitted
four triangles per input triangle into buffers sized for one, and did twice the
rasterization work it needed to in that mode. Removed the output-stage copy, and
gave the function the output capacity so it stops when full - even at 2x, a
culling-disabled draw over ~32k indices doesn't fit.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 22:33:01 +02:00
Henrik RydgårdandClaude Opus 5 b35f28e200 Remote ISO: don't serve the whole filesystem when no folder is set
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 13:51:02 +02:00
Henrik RydgårdandClaude Opus 5 5f9131ed2b Emitters: fix a batch of encoding bugs
Found by a review pass over Common/. Three of these affect code the JITs
actually emit today:

* ARM64 TryMOVI(8) returned true unconditionally ("can always do 8"), but MOVI
  with an 8-bit element replicates imm8 into every byte, so it can only encode a
  byte-uniform value. TryAnyMOVI always tries size 8 first, so it succeeded for
  every constant. MOVI2FDUP(FLT_MAX) - VertexDecoderArm64's Jit_PosFloat - came
  out as "movi v0.16b, #0xff", a quiet NaN, and since FMINNM/FMAXNM return the
  other operand for a quiet NaN, the infinity clamp silently did nothing.
  TryAnyMOVI's replication loop was also shifting by every bit position instead
  of by multiples of the element size, and it now only tries an element size the
  value actually repeats at. Regression test added.

* RISC-V SW()'s stack-pointer compression path called C_LWSP instead of C_SWSP,
  turning a store into a load that clobbers rs2 whenever autocompress is on
  (which RiscVJit and VertexDecoderRiscV both enable).

* LoongArch64 EncodeDFj passed the raw register enum instead of DecodeReg(fj),
  so bit 10 was always set and MOVFR2GR_S emitted movfr2gr.d - live in the
  LoongArch JIT's mfc1 and its FPU/vector compilers.

The rest have no callers today, but are wrong as written:

* ARM64: MOVI/MVNI computed the MSL cmode one too high (MSL #8 is 1100, not
  1101); TryMOVI's MVNI-with-MSL branch passed the value instead of its
  complement; TBZ/TBNZ put the register size in bit 31 where b5 belongs and
  didn't mask the bit index to 5 bits; the LDR/LDRSW/PRFM literal form checked
  the wrong mask for imm19 and wrote it unmasked; FCVTZS/FCVTZU's GPR-
  destination branch skipped DecodeReg and derived the type field from the GPR
  rather than from the float source.
* LoongArch64: LDPTR_D/STPTR_W/STPTR_D all passed Opcode32::LDPTR_W;
  AMCAS_DB_D duplicated AMSWAP_DB_D's opcode; EncodeJK shifted rk by 5 instead
  of 10; BYTEPICK_D masked its shift to 2 bits instead of 3.
* x64: VGATHERDPD/VGATHERQPS/VGATHERQPD used the wrong opcode/W combinations
  (only VGATHERDPS was right).

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 13:44:34 +02:00
Henrik Rydgård e4a0f649fa Merge pull request #22160 from saboten731/pr/libretro-vulkan-sync-index-recreate
Handle libretro Vulkan sync index changes at runtime
2026-08-30 12:36:15 +02:00
Henrik Rydgård 76c7ddfb11 Merge pull request #22170 from hrydgard/upnp-portmanager-fixes
UPnP: fix the exit hang and the CPU spin, and only run the thread when enabled
2026-08-30 10:25:44 +02:00
Henrik Rydgård 480f42c2b8 Merge pull request #22171 from hrydgard/vulkan-surface-init-errors
Vulkan: Report surface init failures instead of asserting later
2026-08-30 10:25:29 +02:00
Henrik Rydgård 054cb1c485 Merge pull request #22168 from hrydgard/more-demangle-clauding
More symbol demangling
2026-08-30 10:25:21 +02:00
Henrik Rydgård 271a27cbd5 Merge pull request #22169 from hrydgard/fpu-exceptions
Interpreter: Implement div-by-zero FPU exception
2026-08-30 01:18:48 +02:00
Henrik Rydgård f7c2c55e83 Vulkan: Report surface init failures instead of asserting later
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.
2026-08-30 01:18:37 +02:00
Henrik RydgårdandClaude Opus 5 2cb0be1bc4 UPnP: fix the exit hang and the CPU spin, and only run the thread when enabled
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 00:37:05 +02:00
Henrik RydgårdandClaude Opus 5 febccfc857 docs: clarify that the destructor flag 0 vs -1 is a caller-side convention
Both mean "do not free" as far as the callee is concerned - it only tests the
sign - but 0 is passed specifically for base-subobject destruction.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY
2026-08-30 00:02:14 +02:00
Henrik RydgårdandClaude Opus 5 23ee83b770 docs: CodeWarrior destructors and their hidden flag parameter
The mangling encodes the source signature, so a demangler correctly prints
"Son::~Son()" - but the emitted function takes a second argument (a short in
a1) and returns "this". Anything deriving a function signature from the name
gets it wrong. Verified against both binaries: the flag is only tested for
being positive, which is what selects the operator delete call, and callers
pass -1 far more often than anything else.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY
2026-08-30 00:01:50 +02:00
Henrik RydgårdandClaude Opus 5 595cfc49b8 lang: regenerate he_IL_invert from he_IL
The file holds the Hebrew strings in visual order, for renderers with no bidi
of their own, but only 101 of its 269 Hebrew entries were actually a reversal
of he_IL. The rest were left unreversed, reversed by word instead of by
character, or garbled outright ("Win" read as gibberish either way round).

Derived them all mechanically instead. The transform reproduces the entries
that were already right, and keeps as single units the things that must not be
spelled backwards: the \n escape, %N placeholders, runs of Latin and digits,
and an & accelerator together with the character it marks.

The [Dialog] save/OSK strings are the exception - he_IL already stores those
reversed, since PPGeDraw draws them and has no bidi, so the invert file just
mirrors he_IL for those 22 keys.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01R9fKXvYBrnqtp1QQGaGWVv
2026-08-29 23:50:59 +02:00
Henrik RydgårdandClaude Opus 5 2f5bb829f3 Demangle: rewrite the SN Systems demangler
The old one was reverse engineered from a handful of symbols and got the
shape of the format wrong - it required a digit right after the kind
character, which most real symbols don't have. Measured against a PSP
executable that shipped with its symbol table intact, it decoded 238 of
4662 mangled symbols, most of those incorrectly.

Worked out properly from that binary, the format turns out to be:

  __0 <kind> <name...> <params> [_ <return type>] [<qualifier>]

where the kind character (member function, free function, operator, data)
is the only thing that says how many name components follow, since nothing
separates the last one from the first parameter. Lengths are letters
(A = 0, a = 26); "5" marks an enclosing namespace; "7...._" is a template
argument list, with "4" plus a compact integer for a non-type argument and
"9<index>A" for a back-reference to one; "T<index>" and "N<count><index>"
repeat an earlier parameter; a trailing "K" is const and a trailing "T" is
a static member function. Also handles __TID_/__T_ (the two halves of a
class's RTTI) and __sti__ (a translation unit's static initializers).

That decodes 4661 of the 4662. The one holdout is an STL symbol whose
template argument is a reference to a member of another template.

Declarator wrapping is shared with the CodeWarrior demangler now, so
pointers to arrays come out as "short (**)[64]" in both.

docs/SNSystemsMangling.md describes the format, marking what's inferred
rather than attested.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY
2026-08-29 23:44:35 +02:00
Henrik RydgårdandClaude Opus 5 1ceae377f7 lang: translate "Enable FPU exceptions"
Left out of 8 languages where I couldn't get the terminology right - they
fall back to the English string.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01R9fKXvYBrnqtp1QQGaGWVv
2026-08-29 23:40:52 +02:00
Henrik RydgårdandClaude Opus 5 bd505b7f1c Interpreter: implement the FPU divide-by-zero exception
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01R9fKXvYBrnqtp1QQGaGWVv
2026-08-29 23:40:48 +02:00
Henrik Rydgård 31daeb4b6c Merge pull request #22165 from NABN00B/slider-button
Fix button size in SliderPopup
2026-08-29 23:34:40 +02:00
Henrik RydgårdandClaude Opus 5 dfc04f3578 Demangle: handle CodeWarrior templates, function pointers and @-symbols
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY
2026-08-29 23:23:57 +02:00
Nab 337ce45a86 Fix button size in SliderPopup 2026-08-29 22:20:09 +02:00
Nab c28eb688e5 Fix button size in SliderPopup 2026-08-29 15:31:39 +02:00