Commit Graph
15881 Commits
Author SHA1 Message Date
Henrik Rydgård 8d8009840e Merge pull request #22511 from hrydgard/android-inputbox-fixes
Claude code review: Android inputbox fixes
2026-10-12 13:20:25 +02:00
Henrik Rydgård 04f5994ea6 Create .nomedia files in the NAND directory when installing firmware
Avoids Android looking for images.
2026-10-12 12:43:36 +02:00
Henrik RydgårdandClaude Opus 5.5 ae68a7e73f Coded Arms: Contagion: Read back the stencil the crosshair checks
The crosshair turns red, and lock-on works, when stencil bit 3 is set at
the centre of the frame, which the game tests by reading the pixel from
VRAM with the CPU (#17936). Hook that and read back the stencil of that
row into the alpha bytes in RAM. The alpha the hardware backends keep
doesn't follow stencil everywhere, so reading color isn't enough.

Adds PerformReadbackStencilToMemory for this (8888 only for now).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 12:15:56 +02:00
Henrik Rydgård 42afcc9fd7 Merge pull request #22516 from hrydgard/savestate-stress
Savestate stress test - results and fixes
2026-10-12 11:22:32 +02:00
Henrik Rydgård fd778f6599 Merge pull request #22515 from hrydgard/ssx-video
SSX On Tour: Recognize the game's memcpy, so its videos show
2026-10-12 10:59:05 +02:00
Henrik RydgårdandClaude Opus 5.5 18816b111a Savestate: Put back hooked instructions some old states saved as hook ops
Some old states have replacement hook ops in memory. Since hooks moved out of
memory, those changed the functions' hashes on load, so they weren't hooked
again, and the CPU hit the op as an illegal instruction. Restore the
instructions this session hooked at the same addresses, as the old hooking did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:52:46 +02:00
Henrik RydgårdandClaude Opus 5.5 9bc239f081 OSK: Skip the old wstring in savestates as bytes
Its characters are as wide as the saving platform's wchar_t, so states saved on
Windows failed the length check on platforms with a 4-byte wchar_t.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:52:46 +02:00
Henrik RydgårdandClaude Opus 5.5 d5db77b736 Sas: End a voice with a bad VAG position on state load instead of failing
States from before 2018 saved idle voices uninitialized, so the position can be
garbage, which made the bounds check reject the whole state (Metal Gear Acid 2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:52:46 +02:00
Henrik RydgårdandClaude Opus 5.5 8662d85d70 Headless: add --frame-stats, flips per emulated second after a state load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:52:46 +02:00
Henrik Rydgård 53047f7cab Merge pull request #22510 from hrydgard/misc-improvements
Misc compat flag updates
2026-10-12 10:25:35 +02:00
Henrik RydgårdandClaude Opus 5.5 b83f2ba7c6 Debugger: Add hle.func.hash, to get a function's hash for ReplaceTables
Returns the analyzed function's start, size and code hash at an address,
and the name of the hardcodedHashes entry already matching it, if any.
Replaces patching a printf into HashFunctions() in the docs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 10:06:16 +02:00
Henrik RydgårdandClaude Opus 5.5 1f89313034 SSX On Tour: Recognize the game's memcpy, so its videos show
The game converts each video frame into RAM and copies it into the
framebuffer with its own memcpy, which we didn't recognize. The hardware
backends never saw the copy and kept showing the last frame the GE drew.
Same function in the EU (ULES00192) and Korean (ULKS46038) versions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 09:39:46 +02:00
Henrik Rydgård 0ee8b5adf8 Merge pull request #22509 from hrydgard/buzz-video
Upload video frames written partway into a framebuffer. Fixes Buzz Master Quiz videos
2026-10-11 23:21:39 +02:00
Henrik RydgårdandClaude Opus 5.5 75865683bb Upload video frames written partway into a framebuffer
Buzz! Master Quiz converts its question videos (sceMpegBaseCscAvcRange)
straight into the middle of the frame it has drawn, at 84,69. The
framebuffer upload only matched writes starting at a framebuffer's
address, so the hardware backends never showed the video. Pass the
written width along and draw just that rectangle into the framebuffer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 22:50:57 +02:00
Henrik Rydgård 38528d4790 Remove obsolete JitInvalidationHack
See #22494
2026-10-11 22:06:06 +02:00
Henrik Rydgård 4e2d3ab8d0 Merge remote-tracking branch 'origin/master' into loadmodule-deferred 2026-10-11 20:00:41 +02:00
Henrik Rydgård 5a3393c0a4 Merge pull request #22508 from hrydgard/savedata-no-memstick
Savedata, sceIo: Behave like hardware with the memory stick removed
2026-10-11 19:59:21 +02:00
Henrik RydgårdandClaude Opus 5.5 2f21ca6837 Savedata, sceIo: Behave like hardware with the memory stick removed
With "Memory Stick inserted" off, the savedata modes that show no dialog now
fail with their own "no memory stick" error, as recorded on a PSP with the
stick pulled out (pspautotests utility/savedata/nomemstick). ms0: opens, stats
and directory opens fail with SCE_KERNEL_ERROR_NODEV.

SOCOM: Fireteam Bravo 2's boot-time AUTOLOAD loaded its save anyway, or said
there was no data rather than no memory stick.

test.py gets a test_flags table for tests that need their own headless flags;
this one runs with --no-memstick.

Fixes #21846

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:30:39 +02:00
Henrik RydgårdandClaude Opus 5.5 0583bdf335 CmdLine: Fix inverse booleans, add --no-memstick
An inverse boolean option was inverted twice, so --windowed forced fullscreen
on. --no-memstick runs with the memory stick removed, for testing that from
headless (which otherwise always inserts one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:30:38 +02:00
Henrik RydgårdandClaude Opus 5.5 4fda53a62e sceKernelLoadModule: Put the module in memory when the wait ends
We copied the module into memory at the start of the call, then made the caller
wait for the read. On hardware the module's bytes only arrive at the end, and
until then other threads can use that memory (pspautotests modules/loadrace).

SOCOM: Fireteam Bravo 2 EU (UCES00543) relies on this. Its loader EBOOT loads
SOCOMPSP.PRX on one thread while another builds a GU display list in a 0x40-byte
buffer at the end of its .bss. That list overruns into the free memory right after
it, which is where the game module goes. On hardware the module load later
overwrites the overrun; for us, the overrun overwrote the module's first
instructions, and the game crashed as soon as it started.

The load now runs from a CoreTiming event at the end of the wait. Pending loads
are saved in savestates.

Fixes #18842

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:08:11 +02:00
Henrik Rydgård 0ce2330b97 Merge pull request #22505 from hrydgard/fix-savestate-zip-install
Fix installing save state zips with certain structures
2026-10-11 18:58:41 +02:00
Henrik RydgårdandClaude Opus 5.5 1b1e3aba1f Fix installing save state zips
Save state zips are detected with stripChars past the last slash, since
the whole directory is stripped. ExtractZipContents then computed a
negative directory length, created a directory named after each file,
failed to open it for writing and reported "Storage full".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 18:27:30 +02:00
Henrik Rydgård e46bd61e2e When taking a screenshot, clear all notifications except achievements. 2026-10-11 17:40:01 +02:00
Henrik RydgårdandClaude Opus 5.5 81efd601f0 iOS: Merge the Metal view controller into the base one
With the GL view controller gone, PPSSPPViewControllerMetal was the only
subclass of PPSSPPBaseViewController. They're now one class,
PPSSPPViewController, in ios/ViewController.mm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:13:29 +02:00
Henrik RydgårdandClaude Opus 5.5 ebab68ac0a iOS: Remove availability checks for versions below the 15.0 minimum
Also drops what they guarded: the pre-14 UIImagePickerController and
document picker fallbacks, the psychic paper entitlement workaround
(fixed in 13.5), and the CS_EXECSEG_ALLOW_UNSIGNED JIT probe, which
returned false on everything from 14.4 on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:13:29 +02:00
Henrik RydgårdandClaude Opus 5.5 5704b972e7 iOS: Remove OpenGL support from the app
The app now always uses the Metal view controller with Vulkan (MoltenVK),
for both the App Store and sideload builds. The GL view controller is gone,
and Common/GPU/OpenGL (except GLFeatures, used by the shader generators) and
GPU/GLES are no longer compiled for it. PPSSPP_API_ANY_GL is now off for
all of iOS except the libretro core, which still uses GLES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:13:28 +02:00
Henrik RydgårdandClaude Opus 5.5 24ea7c4193 Everybody's Golf: Read back the screen under the loading spinner
The spinner copies the displayed framebuffer under it into the transparent
texels of its frames, which came out as stale VRAM around the disc. Hook it
after it gets the framebuffer and read back the rows it covers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 15:10:34 +02:00
Henrik RydgårdandClaude Opus 5.5 94d943664e Everybody's Golf: Hook the player portrait copy too
The first game builds its portrait like Golf 2 and Tennis, but its copy
function keeps y and h in different registers and loads the framebuffer pointer
at a different offset. Move the readbacks into a helper and add a hook for this
layout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 15:06:24 +02:00
Henrik RydgårdandClaude Opus 5.5 6f56798c44 Everybody's Tennis: Hook the player portrait copy too
Tennis builds its portraits like Everybody's Golf 2: it memcpys a 64x64 rect of
the framebuffer and sets each texel's alpha bit from depth read straight from
VRAM, so the circles in the corners came out empty. Its copy function is
scheduled differently and hashes differently, so add it as a second entry for
the same hook, which is renamed to fit both.

Fixes #17826.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 15:00:40 +02:00
Henrik RydgårdandClaude Opus 5.5 fbcec91074 Debugger: Send whole pixels in gpu.buffer.* base64 responses
StreamBufferToBase64 sent width * height bytes, so any buffer with more than one
byte per pixel came back truncated: half a D16 depth buffer, a quarter of a
D32F or 8888 one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 14:50:19 +02:00
Henrik RydgårdandClaude Opus 5.5 9720041ecf Debugger: Add game.state.load
Queues loading a savestate file, for scripting repros that load a state partway
through a run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 14:50:19 +02:00
Henrik RydgårdandClaude Opus 5.5 648766dad4 Everybody's Golf 2: Read back color and depth for the player portraits
The game builds each 64x64 portrait by memcpying a rect of the framebuffer one
partial row at a time, then setting each texel's alpha bit from the depth
buffer, which it reads directly from VRAM. Neither got read back: the row
copies don't line up with a framebuffer, and depth is never downloaded.

Hook the copy function to read back both, whole rows at a time. That needed two
fixes to requested depth copies: they were dropped when the depth buffer lies
above all color buffers, and only an exact full-buffer size matched.

The portrait texture now matches the software renderer, though it still
doesn't show up on screen with Vulkan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 13:25:10 +02:00
Henrik Rydgård f70df399ab Merge pull request #22494 from hrydgard/jit-shadow-block-map
JIT shadow block map: Keep JIT block entries and replacement hooks out of PSP memory
2026-10-11 12:46:42 +02:00
Henrik Rydgård 7572ac05ca Screenshot code cleanup, fix issue where the notification would be in a second fast screenshot
Fixes #22497
2026-10-11 10:56:43 +02:00
Henrik RydgårdandClaude Opus 5.5 5fa4a475f9 Rename the replacement pseudo-op, drop the leftover emuhack defines
With block entries out of memory, the only fake opcode left is the one the CPU cores fetch in
place of a hooked instruction. Call it MIPS_CALL_REPLACEMENT_OP, and its handlers
Int_CallReplacement and Dis_CallReplacement, and remove the sub-op defines and the unused
MIPS_IS_REPLACEMENT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:33:30 +02:00
Henrik RydgårdandClaude Opus 5.5 48973c83c6 Debugger: Show replacement hooks in the disassembly again
Memory no longer holds the hooks, so the disassembly fetches the op the CPU runs, which is the
CallRepl pseudo-op at a hooked address. That lookup can come from the Win32 debugger's UI thread,
so the CPU thread now changes the hook map under a lock (and reads it without, being the only
writer).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:33:30 +02:00
Henrik RydgårdandClaude Opus 5.5 6d122f416e Keep JIT block entries and replacement hooks out of PSP memory
The JITs used to find a block by overwriting its first instruction with an "emuhack" holding
the offset into the code space, and replacement functions installed their own emuhack the same
way. Anything that read code (savestates, the debugger, memory dumps, cheats, the function
hasher, games reading their own code) had to translate them back.

Blocks now live in a block shadow: one u32 per guest instruction, indexed by the PC masked with
BLOCK_SHADOW_MASK (1GB on 64-bit, folding the kernel and uncached mirrors; 256MB on 32-bit, iOS
and Switch, where address space is short), with the low bits cleared so a misaligned PC can't
read across two entries. Only the ranges under memory views are committed, so a bad PC still
faults in the dispatcher, and the shadow is reserved before the memory map so it can't land in
a gap of the 4GB window. Entries hold full 32-bit offsets, lifting the 16MB limit. The
dispatchers on every backend, and the IR interpreter's loop, look blocks up there.

A hooked instruction without a compiled block gets the JIT's hook value in its entry: the offset
of the dispatcher's compile path (1 for the interpreters, the IR arena keeping that offset
empty), so dispatching there compiles a block, and the frontends substitute the hook. Compiling
replaces the entry with the block's, and destroying the block puts the hook value back. The hook
value is rewritten when the JIT changes or regenerates its dispatcher.

Hooks live in a map keyed by the masked address, and are dropped when the game overwrites the
hooked instruction, or when an icache invalidation finds the function's hash (immediates masked,
as when matching it) changed. Those checks run when queued invalidations are processed, since
cheats and the debugger queue them before writing. Installing and removing hooks invalidates
deferred, as it happens in HLE calls that can run inside a block. The cache instruction goes
through MIPSState, so it drops changed hooks too.

With clean memory, the save/clear/restore around savestates, memory reads and dumps goes away,
as do the source-range invalidations in the memcpy replacements, sceDmac and the cheats.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:33:30 +02:00
Henrik RydgårdandClaude Opus 5.5 0a8ec4d8f1 Invalidate code that HLE and the debugger write over
Helper thread stubs, fake syscall stubs and the module start trampoline go into memory that may
have held code (freed kernel memory, a reused thread stack), and the debugger memory views can
edit code. Invalidate what they write, which so far only happened by accident when the write hit
the first instruction of a JIT block.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:33:30 +02:00
Henrik RydgårdandClaude Opus 5.5 59fed92dac IR native: Invalidate linked blocks again
78c75e1a0b dropped IRNativeJit::InvalidateCacheAt as a duplicate, and with it the only call to
the backend's InvalidateBlock. Since then a block linked to an invalidated one kept jumping into
its old code, past the dispatcher. Call it from IRJit::InvalidateCacheAt through a virtual, like
the other native block hooks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:33:30 +02:00
Henrik Rydgård c51daff839 Merge pull request #22498 from hrydgard/sce-audio-reschedule-fix
sceAudio rescheduling fix
2026-10-11 09:22:18 +02:00
Henrik RydgårdandClaude Opus 5.5 76a8e9d45f sceAudio: Don't reschedule before the mixed block is pushed
Waking a thread from the mixer reschedules, which runs any due CoreTiming
events. When an audio tick runs late, the next tick would already be due, and
it would mix over mixBuffer before this one had been output. The next block
would then be played twice and this one dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 00:56:20 +02:00
Henrik RydgårdandClaude Opus 5.5 2cf8b69bab VSH: Drop the 6.6x alarm-category patch
6.60 and 6.61 crashed in vsh_module's alarm-task scan on a count nothing
initialized, and a data patch zeroed it. With the shell's modules started
in order, both reach the XMB, show icons and launch homebrew without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:32:46 +02:00
Henrik RydgårdandClaude Opus 5.5 09974f9e82 VSH: Start the shell's modules one at a time, fixing game icons
The kernel starts modules one by one, each module_start returning before
the next starts. We started them all as threads side by side, and
vsh_module's start ran before paf's. Its 12MB request for game_plugin's
surface pools then went to the 8.3MB heap we pre-filled scePaf's pool
slot with, failed, and game_plugin threw away every decoded ICON0 and
PIC1 (white tiles on 3.80-5.05, grey placeholders from 5.50).

heaparea1, vshbridge, paf, common_gui and common_util now start in order,
and vsh_module waits for the last. The kd/ drivers still start side by
side, since syscon's start blocks on its hardware interrupt. paf fills
its own pool slot now, so the pre-fill is gone. Icons show on every
version from 1.50 to 6.61, and homebrew launches on the same ones as
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:32:46 +02:00
Henrik RydgårdandClaude Opus 5.5 9fa53c8337 HLE: Add sceKernelSetParamSfo and sceMePowerControlAvcPower
The VSH's sceVshBridge_Driver calls both when launching a game, and some
firmware versions give up on the launch unless they succeed (5.xx for the
first, 2.00 for the second). Neither has anything to do here: our HLE
LoadExec reads the PBP itself, and there's no AVC decoder to power.

Added under the NIDs whose names are known: sceKernelSetParamSfo's old,
5.xx and 6.60 NIDs (from DC-M33's NID resolver, Adrenaline's NID table and
uOFW), and sceMePowerControlAvcPower's old and 6.60 NIDs (jpcsp's
psplibdoc, uOFW). Appended at the end of SysMemForKernel, and
sceMePower_driver registered last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:33:34 +02:00
Henrik RydgårdandClaude Opus 5.5 ba4a95d8ff HLE: Send the VSH's LoadExec calls to HLE on every firmware version
The VSH launches games through the VSH variants of LoadExecForKernel,
whose NIDs most firmware versions changed. Where HLE doesn't know the NID,
the firmware's own loadexec.prx ran instead, and the launch got nowhere,
so homebrew launched from the XMB only on firmware 3.73 and older, and
6.60/6.61.

Each variant is the same small wrapper, loadExecVSH(apiType, file, opt,
0x10000), so when loadexec.prx exports one we don't know, look at the API
type it loads into a0, and for Disc (0x120) and MS2 (0x141), overwrite it
with a call to our HLE implementation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:33:34 +02:00
Henrik RydgårdandClaude Opus 5.5 2072e0d568 VSH: Find the homebrew patches by pattern, for every firmware version
Instead of 6.61's offsets, look for the checks by their instructions,
each of which has to match exactly once. Checked against vshmain.prx from
every firmware version, 1.50 to 6.61:

- The DISC_ID check is there from 2.00 on.
- The DATA.PSP header check from 5.00 on, in two forms: 5.xx compares
  the header bytes against ~PSP and ~SCE itself, and 6.30 on test a type
  a helper worked out. 6.00 to 6.20 have both. For the byte check, the
  "first byte isn't ~" branch is pointed at where a good header goes.
- 1.50 and 1.52 have neither, and run unsigned code anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:33:34 +02:00
Henrik RydgårdandClaude Opus 5.5 24a2f6110d VSH: Patch vsh_module so the XMB can launch unsigned homebrew
Picking homebrew in the XMB gave "The game cannot be started. The data
is corrupted." without ever calling LoadExec: vshmain.prx rejects a
PARAM.SFO without a valid DISC_ID, and a DATA.PSP that's a plain ELF
rather than ~PSP. NOP the three branches that do that, the same patches
ProCFW's vshctrl makes and JPCSP applies. They're for 6.61's build, and
are only applied if all three instructions are what we expect.

With it, Kosmodrones launches from the XMB, through the PSP boot logo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:33:34 +02:00
Henrik RydgårdandClaude Opus 5.5 3dddacbfe0 Headless: Add --dump-audio=FILE.wav to record the mixed audio output
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 20:04:37 +02:00
Henrik RydgårdandClaude Opus 5.5 6ed178075c sceUsbMic: Fix QueueBuf::resize losing what was buffered
It allocated the new buffer, pointed buf_ at it, then popped from buf_,
so it copied the uninitialized new buffer onto itself. That happened
whenever a game asked for a larger block than before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 18:55:13 +02:00
Henrik RydgårdandClaude Opus 5.5 6cf4872e78 Android: Hand microphone input over on the CPU thread, like the other hosts
The Oboe input callback used to call Microphone::addAudioData directly,
writing PSP memory from the audio thread. Now it only pushes samples
into a SampleRing, and pollRecording drains it on the CPU thread.
Starting and stopping still go through Java, which asks for permission.

Every host with a microphone polls now, so sceUsbMic drops what only the
push path needed: QueueBuf's recursive mutex, restarting the mic on a
device change, and the startMic parameter nobody read. Also removes the
unused System_AudioRecordingState and Microphone::isNeedInput.

The Java microphone_command branch didn't return true, so every command
logged "processCommand failed".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 18:49:21 +02:00