Commit Graph
100 Commits
Author SHA1 Message Date
Henrik Rydgård 4f8859d0ec Centralize a disassembly utility function between the debuggers 2026-08-11 09:08:52 +02:00
Henrik Rydgård 9396030f02 More Read_U32 cleanup 2026-08-11 09:08:52 +02:00
Henrik Rydgård 3c7a323e99 Merge pull request #22074 from hrydgard/httpclient-fixes
HTTP Client cleanup and fixes
2026-08-11 08:36:07 +02:00
Henrik Rydgård 846fcf3e2c Merge pull request #22071 from hrydgard/webserver-timeout
Fix unexpectedly long timeouts in RegisterServer
2026-08-10 22:58:22 +02:00
Henrik Rydgård 53627fa9ef Merge pull request #22070 from James2022-rgb/feature/font-hw-blend
sceFont: match hardware's glyph blending and destination combine
2026-08-10 14:26:01 +02:00
Henrik Rydgård 914e4e4cab Merge pull request #22069 from hrydgard/read-u32-cleanups
Tech debt: Start cleaning up HLE memory access
2026-08-10 12:09:50 +02:00
Henrik Rydgård ca9c4a0bd5 Merge pull request #22061 from hrydgard/fix-net-file-audit-bugs
Common audit: Fix net/file bugs
2026-08-10 12:08:47 +02:00
Henrik Rydgård feefbfcba0 Apply Nemo's feedback 2026-08-10 11:29:02 +02:00
Henrik RydgårdandClaude Sonnet 5 df751d5571 URL/VFS: fix latent pointer/index UB on empty or tiny input
UriDecode() formed SRC_END - 2 unconditionally, a pointer before the
start of the buffer (UB) for a 0- or 1-byte input. IsLocalAbsolutePath()
indexed path[0]/path[1] on a std::string_view with no bounds check,
UB for an empty path (path[0]) or a 1-byte path on Windows (path[1]).
Neither was known to crash in practice, but both are real UB flagged
by hardened/UBSan builds and easy to trigger (e.g. an empty query
string, or listing the VFS root).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 11:26:39 +02:00
Henrik RydgårdandClaude Sonnet 5 37cea65e0e WebsocketServer: cap message size, avoid UB on empty-vector payloads
ReadFrame() accepted a 64-bit client-supplied payload length with only
a top-bit check, and ReadPending() immediately resized a buffer by it
before any data had arrived - a single frame claiming a huge length
(reachable via the WebSocket debugger endpoint) could trigger a
multi-exabyte allocation attempt. Now rejected up front (both the
single frame and the fragmented-message total) against a 64MB cap.

Also replaced &payload[0]/&vector[0] with .data() in the send/receive
paths - operator[] on a possibly-empty vector (e.g. an empty PING) is
UB even when the result is never dereferenced.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 11:26:24 +02:00
Henrik RydgårdandClaude Sonnet 5 6ee6641fe5 ZipFileReader: guard against implausible/overflowing declared sizes
ReadFile()/ReadSingleFileFromZip() allocated/resized directly off a
zip entry's declared uncompressed size with no sanity check. A
crafted size near UINT64_MAX would wrap ReadFile()'s "size + 1" to 0,
allocating almost nothing while zip_fread() still writes the full
declared size into it - a length-field-driven heap overflow from a
malicious zip/texture pack. Both now reject entries above a generous
4GB cap. Also fixes GetFileInfo() reading zstat.name[strlen(name)-1]
unchecked, which underflows to SIZE_MAX for a zero-length entry name.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 11:26:24 +02:00
Henrik RydgårdandClaude Sonnet 5 99ab8b81ca HTTPHeaders: fix OOB alloc from a request line with no trailing space
ParseHttpHeader() used strchr(buffer, ' ') unconditionally as endptr,
even though the parser explicitly supports HTTP/0.9-style requests
with no trailing space/version (type = SIMPLE). A request line like
"GET /" with no space made strchr return null, and nullptr - buffer
truncated to a garbage length driving new[]/memcpy. Falls back to the
end of the line when no space is found, and clamps param_length to
avoid a similar issue when '?' appears after the (missing) space.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 11:26:24 +02:00
Henrik Rydgård 6540a29180 Merge pull request #22067 from hrydgard/networking-work
Fix some WinSock error code problems, fix error logic in InputSink (networking)
2026-08-10 11:25:46 +02:00
Henrik Rydgård dd87815c07 Merge pull request #22068 from SternXD/winmobile
UWP: Purge Windows Mobile
2026-08-10 11:25:23 +02:00
Henrik Rydgård a1f027bf84 Merge pull request #22057 from hrydgard/frametest-on-ci
Add a minimal framedump-render test system for CI
2026-08-10 11:24:42 +02:00
Henrik Rydgård 2be4d995f2 More Read_U32 cleanup 2026-08-10 11:23:23 +02:00
Henrik Rydgård 7824f69020 Code style updates 2026-08-10 11:17:58 +02:00
Henrik Rydgård e2bc017112 More removals of Read_U32 2026-08-10 11:17:45 +02:00
Henrik Rydgård 2d208ca7d9 Remove Read_U32 from sceKernelMutex 2026-08-10 11:02:25 +02:00
Henrik Rydgård b42157aa49 Core: Add utility function to properly report memory exceptions from HLE functions (when they would actually crash the PSP) 2026-08-10 10:41:32 +02:00
Henrik Rydgård 66c8bfbcb2 Improve semantics 2026-08-10 10:11:24 +02:00
Henrik Rydgård a7b96ce7e0 Add the error flag to OutputSink as well. 2026-08-10 10:08:13 +02:00
Henrik Rydgård 0a1821f5d8 InputSink: Add an error flag. OutputSink: Unify the error handling between unix and Windows 2026-08-10 10:04:21 +02:00
Henrik Rydgård 933751e2a9 Windows: Avoid POSIX error codes leaking into our WSA socket error codes 2026-08-10 10:04:21 +02:00
Henrik Rydgård 82a3ef31b1 InputSink: Inline AccountFill into Fill 2026-08-10 10:04:21 +02:00
Henrik Rydgård 2c4e3e241c Merge pull request #22065 from hrydgard/fix-math-serialize-system-audit-bugs
Common audit: Fix math/serialize bugs
2026-08-10 09:30:51 +02:00
Henrik RydgårdandClaude Sonnet 5 f03f168d96 MemArenaWin32: check CreateFileMapping failure
GrabMemSpace() ignored a NULL return from CreateFileMapping and
returned true anyway, unlike every other platform's arena backend
(Darwin, Android, Posix all check and return false on failure). Under
memory pressure this let startup proceed to CreateView()/MapViewOfFileEx
with an invalid handle instead of failing cleanly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 01:00:28 +02:00
Henrik RydgårdandClaude Sonnet 5 12579e8603 OSD: lock mutex_ in all methods that touch entries_
Update()/Entries()/ClickEntry()/Show()/SetProgressBar()/RemoveProgressBar()
locked mutex_ around entries_, but CancelById/ShowAchievementUnlocked/
ShowAchievementProgress/ShowChallengeIndicator/ShowLeaderboardTracker/
ClearAchievementStuff/SetClickCallback/SetFlags didn't. Core/WebServer.cpp
runs its own thread and calls SetClickCallback() while the main thread's
Update() concurrently erases/iterates the same vector every frame.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 01:00:28 +02:00
Henrik RydgårdandClaude Sonnet 5 1eab737e4d Serialize: guard List/Deque/Map/Set against corrupted size fields
DoVector already rejects an attacker/corruption-controlled size that
would resize far beyond what's actually left in the savestate buffer.
DoList/DoDeque/DoMap/DoMultimap/DoSet never got the same treatment -
a corrupted count field (e.g. 0xFFFFFFFF) drove an immediate huge
resize (list/deque) or an unbounded loop of allocations (map/set)
before any per-element bounds checking kicked in. All five now check
the declared count against PointerWrap::Remaining() first.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 01:00:28 +02:00
Henrik RydgårdandClaude Sonnet 5 60796e08f9 Serializer: fix OOB read loading a std::string from a savestate
MODE_READ built the string via x = (char*)*p.ptr, which strlen()s for
a NUL terminator. CheckRead(stringLen) only guarantees stringLen bytes
are available in the buffer, not that one of them is a NUL - a
corrupted savestate missing the terminator caused strlen to scan past
the checked region. Now uses a length-bounded assign(), matching how
the wstring/u16string siblings already do this correctly via memcpy.
Also tightens the stringLen validity check to require >= 1 (matching
what a real serialized string always has), so stringLen - 1 can't
go negative for a corrupted stringLen of 0.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 01:00:28 +02:00
Henrik RydgårdandClaude Sonnet 5 27dcde7419 Buffer::Printf: fix OOB stack read on vsnprintf truncation
When vsnprintf's return value (the would-have-been length) was >=
sizeof(buffer), the code logged a truncation warning but then still
memcpy'd that full, untruncated length out of the 4096-byte stack
buffer, reading past its end. retval is now clamped to what vsnprintf
actually wrote before use.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 01:00:28 +02:00
Henrik RydgårdandClaude Sonnet 5 be3b417461 expression_parser: bound token length to fix stack buffer overflow
initPostfixExpression() filled a fixed char subStr[256] with no bound
check while tokenizing numeric literals and identifiers. A 256+ char
token in a debugger expression (breakpoint condition, watch,
memory.search) - settable over the network via the WebSocket debugger
- smashed the stack. Now bails out with an error once the token
reaches the buffer size.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-10 01:00:28 +02:00
Henrik Rydgård 66ddc3f5a2 Frametests: bump submodule (maxMse 0.1 for CPU float noise) 2026-08-10 00:46:56 +02:00
Henrik Rydgård 8e40e811c2 Merge pull request #22064 from hrydgard/fix-gpu-render-audit-bugs
Common audit: Fix GPU-related bugs
2026-08-10 00:04:33 +02:00
Henrik Rydgård cf08144ab8 Merge pull request #22062 from hrydgard/fix-data-audit-bugs
Common audit: Fix /data bugs
2026-08-10 00:04:08 +02:00
Henrik Rydgård aa6c0f1805 Merge pull request #22063 from hrydgard/fix-ui-vr-input-audit-bugs
Common audit: Fix UI/VR-input bugs
2026-08-10 00:03:43 +02:00
Henrik Rydgård c3705c7cef Frametests: bump submodule (tolerate 1-pixel run-to-run noise) 2026-08-09 23:01:23 +02:00
Henrik Rydgård 757dd75080 Frametests: bump submodule to Linux-regenerated references 2026-08-09 22:38:46 +02:00
Henrik Rydgård abb57c620f Adjust some log levels 2026-08-09 22:10:54 +02:00
Henrik RydgårdandClaude Sonnet 5 6d35e35e17 VRRenderer: null projections after freeing, guard VR_GetView
VR_DestroyRenderer() freed projections without nulling it, and
VR_GetView() indexed it with no initialized check - a latent
use-after-free if VR_GetView were ever called between a destroy and
the next VR_InitRenderer. Not currently reachable (VR_DestroyRenderer
is only called from inside VR_InitRenderer, synchronously followed by
reallocation), but cheap to close off.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 21:41:54 +02:00
Henrik RydgårdandClaude Sonnet 5 4706a4509d VRInput: fix left controller haptics never firing
INVR_Vibrate()'s channel selection was `i & chan` (bitwise AND) inside
a loop over i in [0,2), instead of just using chan directly as the
index. For chan=0 (left controller, per the only call site iterating
j in [0,2)), i & 0 is always 0, so the "if (channel)" check was never
true and vibration_channel_duration/intensity were never set - the
left controller silently never vibrated. chan=1 (right) happened to
work by coincidence (i=1 gives 1 & 1 == 1). chan is now used directly
as the array index, with no loop needed.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 21:41:54 +02:00
Henrik RydgårdandClaude Sonnet 5 7ff9fbdb47 InputMapping: fix OOB access, reject DEVICE_ID_ANY mappings
FromConfigString() indexed parts[0]/parts[1] from SplitString() with
no check that at least 2 parts were produced - a malformed/truncated
line in a hand-edited controls.ini (no '-') was an OOB vector access.

Separately, DEVICE_ID_ANY (-1) didn't round-trip correctly:
ToConfigString() formats it as e.g. "-1-5", but splitting that on '-'
produces "", "1", "5" instead of "-1", "5" - deviceId decoded to 0 and
keyCode to 1 instead of 5. Rather than special-casing the negative
sign to make it round-trip, just reject DEVICE_ID_ANY mappings
outright - it's not something we want to support, and the whole ANY
concept is likely going away. Preserves the existing (tested) behavior
of tolerating a MultiInputMapping string and parsing just its first
mapping, via atoi()'s stop-at-first-non-digit behavior.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 21:41:54 +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årdandClaude Sonnet 5 1a9f2f827f DrawBuffer::V(): don't overflow verts_ in release builds
The only guard against writing past the fixed 65536-entry verts_
array was _dbg_assert_msg_, which is compiled out entirely outside
_DEBUG - a large enough single batch (e.g. Circle()/CircleSegment()
with a big segment count, or a large UI list without an intervening
Flush()) silently corrupted the heap in release builds. Added a real
bounds check that drops the vertex instead.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 21:31:22 +02:00
Henrik RydgårdandClaude Sonnet 5 ef87fe2e74 ManagedTexture: fix file buffer leak on failed image decode
TextureLoadTask::Run() only freed the VFS-read file buffer on the
success path - a corrupt/truncated image in a texture pack or
replacement texture directory (valid magic, bad payload) leaked the
whole file's bytes on every load attempt.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 21:31:22 +02:00
Henrik Rydgård 0e30353583 CI: fetch the frametests submodule in the test job and always run frametests on Linux
The test set lives in the 'frametests' git submodule (hrydgard/ppsspp-frametest-ci).
The test job previously only inited pspautotests, so the submodule was never
synced on CI and the (now removed) hashFiles gates skipped the frametests steps.
Use an HTTPS submodule URL so CI can fetch it without SSH keys.
2026-08-09 21:15:51 +02:00
Henrik RydgårdandClaude Sonnet 5 b0ab7fbaf7 FastVec::insert: fix off-by-one moving one element too many
The memmove length was computed from size_ after ExtendByOne() had
already bumped it, so it moved (oldSize - pos + 1) elements instead of
(oldSize - pos) - reading one uninitialized element past the old data
and writing one element past the new logical size. Currently masked
by ExtendByOne()'s growth policy always leaving capacity slack, but
a real overflow waiting for that assumption to not hold. Now captures
the old size before extending.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:31:03 +02:00
Henrik RydgårdandClaude Sonnet 5 2360705a43 CharQueue: don't assert when '\r' is buffered but '\n' hasn't arrived yet
next_crlf_offset() called peek() one byte past the currently buffered
data whenever a '\r' was the very last byte received (a normal TCP
fragmentation boundary) - peek() has no way to signal "not enough
data yet" and just asserts. Now checks there's actually a next byte
before peeking.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:31:03 +02:00
Henrik RydgårdandClaude Sonnet 5 c45ceb6e2f ShiftJIS: don't consume the null terminator as a second byte
next() would read a truncated lead byte's "second byte" unconditionally,
even when that byte was actually the string's null terminator - leaving
index_ one past the terminator, so a subsequent end()/next() call read
one byte out of bounds. Now checks for the terminator before consuming
it, returning INVALID without advancing past it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:31:02 +02:00
Henrik RydgårdandClaude Sonnet 5 406033dc3d RIFF/BackgroundAudio: fix OOB reads on short/corrupt WAV chunks
RIFFReader::ReadData() trusted its count argument completely and
memcpy'd straight from the internal buffer with no bounds check.
Hardened it to clamp against the buffer and zero-fill any shortfall,
as defense in depth.

The actual reachable bug was in BackgroundAudio.cpp: it read a WAV
'smpl' chunk into a vector sized by GetCurrentChunkSize(), then
unconditionally indexed smplData[28] (and, for the loop array,
smplData[36]) with no check that the chunk was actually that large -
a short/corrupt chunk in a game's background-music WAV caused a heap
OOB read. Also fixes &smplData[0] being UB when the chunk is empty.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:31:02 +02:00
Henrik RydgårdandClaude Sonnet 5 b322b0621c ReplacedTexture: fix stack OOB write from ZIM mip array contract
LoadZIMPtr() writes width[]/height[]/image[] as arrays (one entry per
mip level, up to ZIM_MAX_MIP_LEVELS) whenever the file has
ZIM_HAS_MIPS set, per its documented contract - but this caller passed
plain scalar locals. A texture-replacement .zim file with that flag
set caused multiple out-of-bounds stack writes. Now passes properly
sized arrays and only uses level 0, matching the existing "we don't
support ZIM mips yet" behavior. Also fixes a pre-existing leak of
image[0] on the "changed since header read" error path.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:31:02 +02:00
Henrik RydgårdandClaude Sonnet 5 44b5a4df74 JSONReader: fix null deref in getInt/getFloat/getBool no-default overloads
These dereferenced get()'s result unconditionally, unlike the
two/three-arg "OrDefault" overloads which check. Hit on externally
sourced JSON: UI/Store.cpp reads the remote homebrew-store listing,
UI/DriverManagerScreen.cpp reads user-supplied GPU driver package
metadata - a field simply missing from either crashed the app instead
of failing gracefully.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:31:02 +02:00
Henrik Rydgård 5ac33863c7 CI: fix upload-artifact version in frametest report step 2026-08-09 19:26:41 +02:00
Henrik Rydgård 472ff6ac71 Merge pull request #22058 from hrydgard/misc-changes
ImDebugger menu improvement, Claude-generated vulnerability and crash fixes (first batch), nemo fixes
2026-08-09 19:19:09 +02:00
Henrik Rydgård d31b475bb2 Minor fixes in Common/Net from Nemo's writeup 2026-08-09 19:03:28 +02:00
Henrik RydgårdandClaude Sonnet 5 78b25599f3 ConsoleListener: fix crash if useThread_ is ever set false
hTriggerEvent/criticalSection/logPending_ were only initialized when
useThread_ was true, but Log() falls back to SendToThread() (which
uses all three) whenever the console isn't open yet, regardless of
useThread_. With useThread_ false, that meant an uninitialized
CRITICAL_SECTION and a null logPending_ write. Dead path today (only
reachable via a commented-out line), but now initializes unconditionally.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:03:28 +02:00
Henrik RydgårdandClaude Sonnet 5 1c0e5ae48c LogManager: fix fp_ race between logging and SetFileLogPath/Shutdown
SetFileLogPath() and Shutdown() closed/reassigned fp_ without holding
logFileLock_, while LogLine() only locked around the actual fprintf,
after already reading fp_ unlocked. A concurrent SetFileLogPath() or
Shutdown() could close the FILE* mid-write from another thread. Now
all three paths hold logFileLock_ across the check-and-use of fp_.
Also fixes fp_ being left dangling (non-null but closed) if
SetFileLogPath() runs while File output is disabled.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:03:28 +02:00
Henrik RydgårdandClaude Sonnet 5 0f792defb4 CountingBarrier: use notify_all instead of chained notify_one
The notify_one chain (each waiter wakes exactly one more) was correct
but fragile and non-obvious. notify_all is simpler and just as cheap
here since Arrive() is not a hot path.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:03:28 +02:00
Henrik RydgårdandClaude Sonnet 5 973a10473a ParallelLoop: make loop tasks cancellable to avoid hang on Teardown
SimpleParallelTask/LoopRangeTask never overrode Cancellable(), so a
ThreadManager::Teardown() racing with an in-flight parallel loop would
drop them without ever calling counter_->Count(), leaving anyone
blocked in WaitableCounter::Wait() stuck forever. Cancel() now counts
down the same as a normal completion.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:03:28 +02:00
Henrik RydgårdandClaude Sonnet 5 dcf742edb4 Dsable AndroidHwScale by default
AndroidHwScale defaults to 0 instead of the old device/resolution-based heuristic (which is
removed along with DefaultAndroidHwScale()).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY

...
2026-08-09 19:03:08 +02:00
Henrik RydgårdandClaude Sonnet 5 ceb3e9a0ad ThreadManager: fix teardown crash and task leak, remove dead code
- ~ThreadManager() never stopped worker threads before freeing the
  global context. If Teardown() wasn't reached before process exit
  (e.g. an early return between Init() and a caller's shutdown path),
  the still-running threads kept touching freed mutexes/queues/condvars,
  causing intermittent crashes on exit. The destructor now tears down
  if still initialized.
- Teardown() silently leaked any non-cancellable task still sitting in
  a queue (global or per-thread) at shutdown time - such tasks were
  never run nor released, since nothing drains those queues once the
  worker threads have been marked cancelled/joined. Now every queued
  task is properly cancelled-or-warned and released.
- Added an assert against a divide-by-zero in EnqueueTask's round-robin
  fallback, which would trigger if Init() were ever called with zero
  compute threads.
- Removed Task::id()/Kind() and ThreadManager::TryCancelTask(), which
  were unused dead code (TryCancelTask was a no-op with no callers).
- Fixed a stale comment on EnqueueTaskOnThread referencing a parameter
  that doesn't exist, and an auto-typed ternary that misleadingly read
  like a deque array copy (it's a pointer, not a copy).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
2026-08-09 19:03:08 +02:00
Henrik Rydgård b6e7cb915a Texture cache management: include w and h in more log messages for context 2026-08-09 19:03:08 +02:00
Henrik Rydgård a41654cf61 More menu changes 2026-08-09 19:03:08 +02:00
Henrik Rydgård 07e63c345c Add SAVE_STATE to BrowseFileType 2026-08-09 19:03:08 +02:00
Henrik Rydgård 63ef2a7498 Hide savestate controls if you are running a frame dump 2026-08-09 19:03:08 +02:00
Henrik Rydgård 4346534e15 Merge pull request #22060 from hrydgard/imdebugger-fixes
Fix the point in time in the frame that we run and render the ImDebugger
2026-08-09 19:00:37 +02:00
Henrik Rydgård c65eb7d54f Fix the point in time in the frame that we run and render the ImDebugger
Turns out it matters quite a lot. Will see if I can clean this up later.
2026-08-09 18:29:33 +02:00
Henrik Rydgård 1fd9d3b24b CI: run frametests on the Linux test job and upload the report
Extend the existing 'test' job (Linux leg) with the frametests run after
the pspautotests, plus an artifact upload of the output so failures can
be examined. Both steps are gated on the test set being present in the
checkout, since dumps, references and config are maintained separately
from the runner.
2026-08-09 14:00:37 +02:00
Henrik Rydgård 0d47851e92 Add the frametests repo as a submodule 2026-08-09 13:41:39 +02:00
Henrik Rydgård 7836d8c202 Frametests: variant suffix/compare-suffix for shared references, output dir 'out'
Variants can now declare a compare-suffix (defaulting to the variant's own
suffix) to compare against another variant's reference images, enabling
shared references across e.g. opengl/vulkan when their output matches.
Reference images are only generated for variants whose suffix equals their
compare-suffix; other variants fail clearly if the shared reference is
missing. Rename the default output directory from frametest-out to out.
2026-08-09 13:41:39 +02:00
Henrik Rydgård b0db6f86ac Scripts: use plain python in shebangs 2026-08-09 13:41:39 +02:00
Henrik Rydgård b0f070040b Don't auto-enable logging in debug mode in headless, it's annoying. 2026-08-09 13:41:39 +02:00
Henrik Rydgård 8c9bf04add Headless: force opaque PNG screenshots by default, add --screenshot-keep-alpha
Games often use the framebuffer alpha channel for non-visual purposes,
so saved PNGs could look fully transparent in image viewers. Force alpha
to 255 when writing PNG screenshots unless --screenshot-keep-alpha is
passed. The MSE comparison ignores alpha either way.
2026-08-09 13:41:39 +02:00
Henrik Rydgård 032781e301 Add headless.py wrapper: locate newest PPSSPPHeadless and forward all arguments 2026-08-09 13:41:39 +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 b67667cbd1 Merge pull request #22053 from hrydgard/qt-removal
Delete all support for Qt
2026-08-08 20:10:54 +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 42ffb42c12 Merge pull request #22052 from hrydgard/win32-debugger-threadsafety
Refactor the old Win32 debugger to be thread safe
2026-08-08 18:01:13 +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årdandClaude Sonnet 5 3500293402 SymbolMap: route remaining GUI-thread callers, remove internal locking
Windows/MainWindowMenu.cpp's Load/Save/Clear Symbol Map menu actions
mutated g_symbolMap directly from the WinMain thread; route them through
Core_RunOnCPUThread like the rest of the debugger. Windows/MainWindow.cpp's
WM_USER_GET_MODULE_INFO handler read it directly too; guard with
g_frameMutex. Windows/main.cpp's SortSymbols() calls turn out to already be
safe as-is - both notifications that trigger them (BOOT_DONE,
SYMBOL_MAP_UPDATED) are only ever fired from the CPU/NativeFrame thread -
so just fix the stale comment claiming reliance on the (now removed)
internal lock.

With those covered, every remaining caller is either on the CPU/NativeFrame
thread already or routes through Core_RunOnCPUThread/g_frameMutex, so
SymbolMap's internal recursive_mutex is redundant - remove it and all ~40
lock_guard call sites. Qt's mainwindow.cpp still pokes at g_symbolMap
directly and unguarded (a pre-existing issue, out of scope - Qt isn't a
maintained backend), but removing the lock doesn't change its public API,
so it still builds.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 17:22:12 +02:00
Henrik RydgårdandClaude Sonnet 5 5df8c14f1e Core_RunOnCPUThread: drain the queue from NativeFrame(), not just Core_RunLoopUntil()
Core_RunLoopUntil() is only reached while a game is actually loaded and
running (via EmuScreen). Anything calling Core_RunOnCPUThread() while at
the main menu with no game loaded would hang forever waiting for a queue
that was never drained. Call Core_ProcessCPUQueue() directly from
NativeFrame(), just before screenManager->render(), so it always runs;
Core_RunLoopUntil() still also drains it for the tight-spin-while-stepping
case once a game is running.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 17:22:12 +02:00
Henrik RydgårdandClaude Sonnet 5 180becf883 BreakpointManager: remove internal locking, now redundant
Every caller either runs on the CPU thread already, routes mutations
through Core_RunOnCPUThread, or holds g_frameMutex for reads - audited
across WebSocket subscribers, the legacy Win32 debugger, ImDebugger, and
the JIT/interpreter backends. Also renames GetMemCheckLocked to
FindMemCheckInRange since it no longer implies a lock is held.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
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årdandClaude Sonnet 5 f72709feb0 Legacy Win32 debugger: fix the painting problem with a frame-scoped mutex
Debugger windows (register list, disassembly view, memory view, breakpoint/
thread/module/stack lists, watch list) read CPU-thread-owned state directly
from the GUI thread's WM_PAINT/list-fill handlers, racing against the CPU
thread. Routing every read through Core_RunOnCPUThread would be too slow for
something invoked continuously on paint/list-refresh.

Add g_frameMutex (Core.h/Core.cpp), held by NativeFrame() only across the
span where it actually touches that state (running the CPU, processing
breakpoints, running the ImGui debugger) - not across input handling or the
present/frame-pacing waits. Debugger windows now hold the same mutex while
reading, giving synchronized reads without the round-trip cost of queuing
to the CPU thread.

CtrlRegisterList::onPaint() goes back to always reading live values (now
safe under the lock) and grays them out by color alone while the core is
running, rather than the earlier snapshot-caching approach.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 17:22:12 +02:00
Henrik RydgårdandClaude Sonnet 5 03dcfd3931 WebSocket debugger: fail fast on invalid memory ranges, move base64 off the CPU thread
memory.read_u8/u16/u32/read/readString/write_u8/u16/u32/write/search all
validated their address/size parameters (and, for search, the rest of its
param parsing) after already queuing onto the CPU thread. None of that
depends on CPU-thread-owned state, so do it upfront instead and fail fast
without a round trip through the queue for requests we already know are
invalid.

Also, for memory.read and memory.readString, only the raw memory copy
(which needs replacements/emuhacks disabled) now happens on the CPU
thread - the base64 encoding itself happens back on the WebSocket thread
afterward, so a large read no longer blocks the CPU thread's frame pump
for the encoding work too.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 17:22:12 +02:00
Henrik RydgårdandClaude Sonnet 5 25c3c90c94 Legacy Win32 debugger: route mutations through the CPU thread
The Win32 debugger dialogs mutate CPU-thread-owned state (breakpoints,
symbol map, registers, memory, kernel threads) directly from the GUI
thread with no synchronization, same problem the WebSocket debugger had.
Route all of these through Core_RunOnCPUThread instead, following the
same pattern used there.

Also drops two forced-pause (Core_Break/Core_WaitInactive/Core_Resume)
dances in CtrlMemView::onChar and DumpMemoryWindow's dump handler, now
unnecessary since Core_RunOnCPUThread works whether the CPU is running
or already stepping.

Modal dialogs (MessageBox, InputBox_GetString, BreakpointWindow::exec,
etc.) are kept outside any queued callback so they never block the CPU
thread on user input. Pure reads/painting (disassembly formatting,
search, list reloads, onPaint) are intentionally left as-is for now.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 17:22:12 +02:00
Henrik Rydgård 1f6f663e2a Merge pull request #22051 from hrydgard/web-debugger-access
Refactor the websocket debug interface to be thread-safe
2026-08-08 13:26:38 +02:00
Henrik RydgårdandClaude Sonnet 5 212ed7cdda Debugger: Route HLESubscriber through the CPU thread
Same treatment as the other subscribers: thread list/wake/stop, function and
data symbol list/add/remove/rename/scan, and backtrace now route their
kernel thread, symbol map, and disassembly manager access through
Core_RunOnCPUThread() instead of touching that state directly from the
WebSocket handler thread.

Memory::IsValidRange() checks that only depend on the request's own
address/size params (not on anything CPU-thread-owned) stay outside the
queued callback and fail fast, rather than making a pointless round trip
through the CPU thread for a request already known to be invalid.

hle.func.scan carries the same unbounded-range caveat already noted for
memory.search: no cap on 'size' beyond valid memory range.

Replaced remaining `auto` locals with concrete types.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 12:40:18 +02:00
Henrik RydgårdandClaude Sonnet 5 75b9df8384 Debugger: Route MemorySubscriber through the CPU thread, drop the forced-pause dance
Every memory.read*/write*/search endpoint used to call LockMemoryAndCPU(),
which - unless the CPU was already stepping - busy-waited for coreState to
settle, force-paused a running game with Core_Break(), and blocked on
Core_WaitInactive() before touching memory, just to get exclusive access
from the WebSocket handler thread. It also took MIPSComp::jitLock around
saving/restoring emuhack ops for the same reason.

Now the whole body of each handler runs inside Core_RunOnCPUThread(), so
none of that is needed for CPU-thread exclusivity: reads/writes happen
inline on the CPU thread itself, whether the game is running or stepping,
without ever pausing it. Confirmed live that memory reads/search now
complete while coreState stays CORE_RUNNING_CPU throughout - no more
stepping flicker on every debugger memory poll.

Kept Memory::MemoryInitedLock (guards against Memory::Shutdown() racing in
from a different thread, e.g. the UI thread stopping the game - unrelated
to the WebSocket-thread-vs-CPU-thread problem) and MIPSComp::jitLock around
the emuhack save/restore (guards against a UI-triggered CPU core switch,
also a different thread than the one Core_RunOnCPUThread targets).

Same caveats as previous conversions: memory.read for a very large 'size'
now base64-encodes on the CPU thread itself, and memory.search still has no
size cap - both will now block the CPU thread's own frame pump for their
duration on a large enough request. Noted inline, not fixed here.

Replaced remaining `auto` locals with concrete types.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 12:15:04 +02:00
Henrik RydgårdandClaude Sonnet 5 c32df3d931 Debugger: Route BreakpointSubscriber, DisasmSubscriber, and most of
CPUCoreSubscriber through the CPU thread

Continues the pattern started with SteppingSubscriber: route breakpoint,
disassembly, symbol, and register access through Core_RunOnCPUThread()
instead of touching that state directly from the WebSocket handler thread.

Two intentional exceptions, matching the reasoning already used for
cpu.stepInto's "not currently stepping" branch:
- cpu.stepping's Core_Break() call stays unqueued - it's what makes the CPU
  thread start reaching the queue drain point in the first place.
- cpu.status stays unqueued - it's meant to be a cheap, frequently-pollable
  status check, and its "pc" field is already documented as inaccurate unless
  stepping. Matches how SteppingBroadcaster already reads the same state
  directly from the WebSocket thread.

Where a handler's response doesn't depend on anything the queued lambda
computed (plain add/remove endpoints), moved req.Respond() back out after
the Core_RunOnCPUThread() call for readability - the JSON building and
socket write happen later in Finish() regardless of where Respond() is
called, so there's no thread-safety difference either way, just clarity
about what actually needs to run on the CPU thread.

memory.searchDisasm carries the same caveat flagged for memory.search: its
scan range has no size cap, so if the CPU is stepping, a very large range
will now block the CPU thread's own frame pump for the scan's duration
rather than running unqueued on the WebSocket thread as before. Not fixed
here - noted in a comment at the call site.

Replaced remaining `auto` locals in these three files with concrete types.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 11:56:32 +02:00
Henrik RydgårdandClaude Sonnet 5 29825c1e61 Debugger: Route the rest of SteppingSubscriber.cpp's handlers through the CPU thread
Extends the cpu.stepInto treatment to cpu.stepOver, cpu.stepOut, cpu.runUntil,
and cpu.nextHLE: each now routes its breakpoint/stepping manipulation through
Core_RunOnCPUThread() instead of touching it directly from the WebSocket
handler thread. cpu.runUntil didn't have an explicit "must be stepping"
guard to begin with; since the CPU-thread queue is now drained unconditionally
at the top of every Core_RunLoopUntil() iteration (not just while stepping),
queuing from it is safe regardless of current core state.

Also corrects a stale comment on Core_RunOnCPUThread() left over from before
the drain point moved from Core_ProcessStepping() to the top of
Core_RunLoopUntil() - it's not limited to the stepping/paused case.

Replaced remaining `auto` locals in this file with concrete types.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 11:32:01 +02:00
Henrik RydgårdandClaude Sonnet 5 c6fccefa49 Debugger: Route cpu.stepInto's stepping state through the CPU thread
The WebSocket debugger's cpu.stepInto handler ran entirely on the WebSocket
handler thread, directly manipulating breakpoints and stepping state (via
Core_RequestCPUStep, g_breakpoints.SetSkipFirst, etc.) that's otherwise only
ever touched from the CPU thread (the one that calls Core_RunLoopUntil, and
thus indirectly NativeFrame).

Adds Core_RunOnCPUThread() - queues a function to run on the CPU thread and
blocks the caller until it's done. The queue is drained at the top of
Core_RunLoopUntil()'s loop, so it's reached continuously (in a tight spin)
while the CPU is stepping/paused, and at least once per call even while fully
running.

cpu.stepInto is the first consumer: once the CPU is already stepping, the
breakpoint/stepping manipulation is now routed through Core_RunOnCPUThread
instead of happening directly on the WebSocket thread. The "not currently
stepping" path still calls Core_Break() directly from the WebSocket thread,
since it's already documented free-threaded and is what makes the CPU thread
start reaching the queue-drain point in the first place.

More WebSocket debugger commands can be converted the same way going forward.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 11:21:46 +02:00
Henrik RydgårdandClaude Sonnet 5 b1f0112cef Debugger: Remove opcode-fusion display and fix cpu step size units
DisassemblyManager used to fuse lui+addiu/load/store into single pseudo-
instructions ("li", fused loads/stores) for display. This only applied to a
handful of opcodes, complicated DisassemblyManager, and was the root cause of
a stepping bug: Core_PerformCPUStep's Into/Over cases treated stepSize as a
byte count, while the WebSocket cpu.stepInto handler computed it as an
instruction count (needed to step over a whole fused macro in one go) - so a
plain, non-fused stepInto silently executed zero instructions.

Removed the fusion logic entirely (DisassemblyMacro, DISTYPE_MACRO) - every
disassembly line is now exactly one 4-byte instruction. With that,
"how many instructions does this line span" is always 1, so the
getInstructionSizeAt() byte-size queries in the legacy Windows and ImGui
debuggers are gone too; step requests just pass 1. Core_RequestCPUStep's
stepSize is now consistently in instructions everywhere.

Also fixes the PPSSPPHeadless build, broken since 0ed1f3e added
OpenWebDebugger() (which calls System_LaunchUrl) without a headless stub.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 11:16:36 +02:00
Henrik Rydgård 0ed1f3eceb Add some easy ways to get into the web debugger 2026-08-08 10:13:09 +02:00
Henrik Rydgård 4d94022af1 Merge pull request #22050 from hrydgard/imgui-updates
ImDebugger menu update, bugfixes
2026-08-08 01:09:20 +02:00
Henrik Rydgård a957086eeb Try to fix test failure (vary strange) 2026-08-08 00:45:11 +02:00
Henrik Rydgård 0d92f49b35 Qualcomm Turnip mesa driver: Remove version check for Bugs::COLORWRITEMASK_BROKEN_WITH_DEPTHTEST
Fixes #22031
2026-08-07 23:52:22 +02:00
Henrik Rydgård 459f4473af Fix crash in GE debugger
Fixes #22027
2026-08-07 23:49:30 +02:00
Henrik Rydgård 8d2c1e7a66 Make the ImDebugger a bit less crash prone outside of games 2026-08-07 22:40:05 +02:00
Henrik Rydgård b4c476d5ed Merge pull request #22048 from hrydgard/imgui-lift
ImDebugger: Lift it out of the EmuScreen
2026-08-07 19:13:13 +02:00