- Fixes#22675
Since 3aa4714ce0 desktop Windows draws text with DirectWrite, which looked
up every font in a collection of only our bundled fonts, so an override
like "Font = 微软雅黑" silently fell back to Roboto. Fall back to the
system font collection when the family isn't one of ours.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A patch drawn as lines is a zigzag per quad (gpu/probe exp175), but the GE
draws a quad's right edge only in the patch's last column, since the next
quad's left edge is the same line. Edges between patches are still drawn
twice. Opaque lines can't show the difference; NFS Most Wanted's
alpha-blended spline visualizer (#22104) came out brighter wherever the
edges were drawn twice. gpu/probe exp257 counts the draws per pixel with
additive blending, for Bezier and spline patches; both are now exact, as is
the NFS frame against the PSP.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cpu/cop0/eret, recorded on firmware 6.61, has eret go to ErrorEPC and clear
only ERL with Status.ERL set, otherwise to EPC clearing EXL, with no delay
slot. Int_Eret already does that; the test joins tests_good.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sceKernelRegisterIntrHandler, sceKernelReleaseIntrHandler,
sceKernelEnableIntr and sceKernelDisableIntr now keep a base handler per
interrupt, and return what tests/intr/kernel/intrhandler recorded on a PSP:
80020065 for an interrupt number out of range (or no handler), 80020067 for
a second handler, 80020068 for one that isn't there.
Faking success for these used to stall the VSH boot, which is why they were
left unresolved. It doesn't anymore: the firmware's drivers make the same 31
calls (interrupts 4, 12, 15-18, 20-24 and 31), and 6.61 reaches the XMB on the
JIT and the interpreter. Nothing raises those interrupts yet, so the handlers
are only kept, and saved in savestates (IntrHandler section version 2).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Kernel code's loads and stores may reach hardware registers, which only the
interpreter handled, so the IR frontend sent every one of them (in kernel
blocks, only run by the VSH boot) to it.
They now become LoadKernel*/StoreKernel* IR ops (one per size, like the
plain loads and stores), which check at run time whether the address is
memory or a hardware register (MMIO). After constant
propagation, ResolveKernelMemory turns the ones at a constant address that's
plain memory into ordinary loads and stores, so only the hardware registers,
and addresses only known at run time, stay checked. The native JITs run the
two ops through their IR fallback. They're barriers, since a register access
has side effects, but can't change the GPRs, so constants stay known across.
The kernel mode check for MMIO moves to the interpreter, the only caller that
can run user code there. TestMMIO covers the new paths and which accesses
stay checked. Under --vsh, the JIT and the interpreter now make the same
unhandled register accesses, in the same numbers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PPSSPP has access to the URI, so a frontend or adb can launch the game with it,
which isn't true of a plain path on modern Android.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The interpreter's MMIO dispatch was a hardcoded chain for GPIO and the syscon
serial port. Devices now register an address range in Core/HW/MMIO and get
each access with its width and the MIPSState making it, for registers that
answer each CPU differently. An access no device handles is logged and reads
as poison, as before.
GPIO and syscon serial become the first two devices. They're reset at boot,
which they never were, and saved in savestates (SaveStart section version 4).
ll/sc, lwl/lwr and lwc1/swc1 reach MMIO in the interpreter too, and kernel
blocks leave lwc1/swc1 to it like the other loads and stores. swl/swr and
lv/sv still treat it as a bad address. TestMMIO runs a kernel-mode access on
each core and the registers through a savestate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The COP0 register file was a static array in the interpreter, shared by
anything that ran kernel code and lost on a savestate load. It's now part of
MIPSState and saved with it (MIPSState section version 5).
eret returns to EPC, or to ErrorEPC with Status.ERL set, as MIPS32 defines
it; that isn't checked against a PSP yet. It and halt carry a new ENDS_BLOCK
flag: the IR frontend ends the block after them and exits to whatever PC the
interpreter left, and the interpreter's IR-matching block timing stops there
too. TestCPUEret runs it on the interpreter, IR interpreter and JIT.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#18528
Launchers typically open the game with ACTION_VIEW and a content:// URI, so
they could never pass options before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The reload reset them, so options passed through the Args extra (like
--pause-menu-exit) were lost before the game even started.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#18456
For external launchers (Android handhelds like the Retroid Pocket), which want
control back when the game ends. Before, only the pause menu's Exit quit the
app; now the game exiting by itself, a stop request and dismissing a load
error do too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
IRJit is now the base the IR JITs share: the frontend, the table of guest
blocks, and compiling and invalidating them. IRInterpreterJit and
IRNativeJit are the two kinds. Only IRInterpreterJit keeps IR, in its arena,
along with its dispatch loop and per-block profile.
The native JITs no longer keep the IR they compiled from. The debugger and
the --jit-profile report compile a native block's IR again when they want it,
on a copy of the frontend's state, which gives what ran unless the code or
that state has changed since.
IRBlockCache and IRBlock become JitBlockTable and JitBlock: where each block
came from, the page lookup for invalidation, and the block shadow entry (an
arena or native code offset, depending on the JIT). One IRJitDebugInterface
serves both JITs, taking the MIPS code from the table and the IR and host code
from the JIT, in place of the native JIT's own debug interface.
The native backends don't see the table at all. They compile a span of IR for
a block number and start PC, and keep what they need about their own blocks
(start PC, code offset, which block starts where, for linking) next to the
exits they already tracked. The register cache takes the instructions
directly.
The tracer's block index now goes into the IR before the block is stored, so
native blocks get it too. The unused FindByCookie goes. TestJit checks that
the native JIT keeps no IR, and can still show it. The JIT stats overlay no
longer shows IR for the native JITs. MIPSState can't be copied, now that it
owns its JIT.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nothing is evicted until the cache estimate reaches these, and they were tuned when the estimate
counted every texture at its PSP size. Counted at real size, upscaled textures crossed 16MB scale^2
times sooner (and re-scaling an evicted one is slow), and a single 2048x2048 replacement filled the
4MB secondary cache. 128MB covers what upscaling up to about 3x effectively had before. Also remove
TEXTURE_KILL_AGE_LOWMEM, unused since the low memory mode went away.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
linksTo_ maps an exit's target PC to the blocks exiting there, but
EraseAllLinks erased the entry keyed by the block number instead. The dead
block's exits were cleared anyway, so its stale entries only piled up until
the next cache clear.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The game boots until the queued state is read, decompressed and applied, which takes a varying amount
of host time, and those boot frames counted towards the timeout. So the same state ended on different
frames depending on the host, for example while texture replacements competed for the IO threads.
After the load the emulation is identical: syscall traces of Fate/Extra CCC with no texture pack, and
with one loading slowly or instantly, are byte for byte the same.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Texture replacement looks up files from several IO threads at once, while another thread may be
reading from the same archive. libzip isn't thread safe, and a name lookup writes to the archive: it
caches each name's guessed encoding, and allocates a converted copy of CP437 names.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A texture waiting for its replacement was only polled while the frame's replacement budget lasted,
so if something used the budget up early in every frame, the textures drawn after it kept their
originals until the cache was cleared (like when minimizing on Android). Poll every time, and only
let the rebuild wait for the budget, keeping TO_REPLACE set so it's retried.
Possibly related to #21586.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The cache decides when to start evicting textures by their estimated memory use, which only counted
the PSP size. A 4x replacement takes 16 times that and texture packs often go higher, so with a big
pack the cache could hold far more than it meant to before evicting anything. Record the size when
the texture is built. Unscaled textures without replacements get the same estimate as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#18746
Packs are mostly made on Windows, where "portraits/x.png" in textures.ini finds Portraits/x.png. On a
case sensitive file system (Steam Deck, Linux) it didn't, while zipped packs already ignored case.
DirectoryReader gets an opt-in ignoreCase mode, used for texture packs. On the first miss it indexes
the directory by lowercased path and retries there, so packs where every name matches never pay for
it, and the walk happens on the IO thread that loads the texture.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The copies went through the thread pool, where they could wait behind other tasks like pipeline
compiles, and one core already saturates memory bandwidth.
A filename with a parent dir component left the texture in CANCEL_INIT, which the texture cache
keeps polling forever. Treat it like a missing file instead. Also remove an unused lastUsed field.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The case insensitive lookup compares against every name in the zip, while an exact one uses libzip's
hash table. Texture packs can have tens of thousands of files.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Saving level 0 marked the whole texture as saved, so the other levels were skipped. Track it per level.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#19623
With a fake mipmap change (Japanese Tactics Ogre), each PSP level is cached and hashed as its own
2D texture, but since 73c06bb776 (v1.14) the replacement and dump code used the PSP level as the
replacement level. Loading a replacement for a level-2 font sheet asked for level 2 of a one-level
replacement and hit the "Invalid miplevel" assert, and dumps were named _2, which the ini then
treats as an ignored texture. Use the created texture's level for both again, as 1.13.2 did.
Also don't force mip filtering from a mipmapped replacement when only one level was uploaded.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The global MIPSComp::jit becomes MIPSState::jit, created and deleted by the
state it runs. JitAt() compiles with the JIT of the state it's handed, so
the dispatchers no longer go through a global. Code that is about the main
CPU by nature (the UI, the crash handler, the disassembler's symbol lookup,
the JIT harness) uses mipsr4k.jit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The JIT backends' calls out of generated code (the MIPS and IR interpreter
fallbacks, and the bad address report) now take the state the backend was
built for instead of reading currentMIPS. Likewise the interpreter's tick
check, the IR dispatcher's Advance, and a couple of debugger helpers.
MIPSState::InitRegisters() no longer sets currentMIPS; the boot paths set it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#22647
sceNetInetBind rewrote the address whenever the ad hoc server was set to
localhost (to 127.0.0.x, even an explicit address) or the built-in ad hoc
server was enabled (INADDR_ANY to the address facing the ad hoc server),
breaking infrastructure sockets. Both rewrites now only apply while
adhocctl is initialized, which keeps ad hoc as it was.
sceNetApctlGetInfo reported its IP from the same ad hoc-relative
getLocalIp(), so a localhost ad hoc server told infrastructure games their
IP was 127.0.0.x. It now uses the host interface's IP (getHostLocalIp(),
split out of getLocalIp()).
Adds the net/inet/bind test, which failed every bind under headless (whose
ad hoc server is localhost).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Without WSAStartup, every sceNetInet socket call failed on Windows with
WSANOTINITIALISED, so no inet test could run there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#22104
NFS Most Wanted's music visualizer is a 32x16 spline of lines at 8x8
tessellation: 144768 indices, which software transform expands to far more
than VERTEX_BUFFER_MAX vertices, so SubmitPrim splits it and flushes between
the parts. The curve's indices sat at the start of decIndex_, where each
flush writes its generated and expanded indices, so the first flush
overwrote most of the later parts and about half the visualizer was lost.
Tessellate the indices into the upper half instead, and don't use the
predecoded output for a curve that will be split, as the flushes decode
over it.
The game also sends an empty PRIM with VADDR 0 every frame, which the
hardware backends logged as a bad vertex address. Skip zero-count PRIMs
before the address check, as the software renderer does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A partition memory block restored its allocator through
BlockAllocatorFromID, whose privilege check asked the thread current
before the load, already deleted with the kernel objects. Loading a
kernel-partition block under a user thread left it without an allocator,
crashing when it was freed. The saved id maps straight back now, and the
current thread is cleared along with the objects.
- The thread LoadExec leaves waiting (with no timeout) for plugins and VSH
modules to start wasn't saved, so a state saved while one was starting
hung the game thread after loading. The VSH modules still to start
weren't saved either, nor cleared.
- A state without the exit callback id left the previous session's.
- From a broken or crafted state: thread ids that GetFast trusts (current,
idle and ready), interrupt handler numbers, FPL and TLSPL indices, and a
MIPS call or action slot that isn't there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Savedata list entries lost their directory across a load. Confirming a
delete in a list dialog loaded from a state failed with "delete failed"
and deleted nothing. LISTALLDELETE can't rebuild it from the names, so
it's saved now, with the broken flag.
- Registry values the game wrote (CLANNAD's font plugin registers its font
there) weren't saved, and a state without a sceReg section wiped them.
- Slots sharing the "no save" icon came back as separate images of one
texture, which Clear() then freed once per slot.
- Old directory states left a file opened with TRUNCATE due for truncation,
so it read as empty and was truncated on close. Old async IO states were
loaded on top of this session's results.
- From a broken or crafted state: OSK keyboard and Korean composition
indices, gamedata install file counts, font ownership modes (an internal
font could be deleted) and FontLib's per-font vector sizes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A review of their savestates found nothing wrong with a normal save and
load, but a broken or crafted state could crash or write out of bounds:
- Atrac2's context pointer was dereferenced and its SAS buffer index used
unchecked. Atrac v1's file size sized an allocation before it was known
to be in the file, and its first buffer could be larger than the file.
- MediaEngine's MPEG header size and read position index a fixed array,
and its ringbuffer size sizes buffers.
- AuCtx's PCM buffer must hold two frames, as the game side requires. And
AuDecode compared a decoded frame with the whole PCM buffer instead of
the half it fills, so a frame between the two zero-filled a negative
size.
- BufferQueue's fill level, VAG block positions (now ending the voice,
like the other bad VAG state), sceAudiocodec's context count, the SRC
mix frequency (divided by), and old sceMpeg states' ringbuffer address.
- SAS decoding with an atrac id that has no atrac crashed after warning.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each due alarm or vtimer queues its id next to a pending interrupt, which
pops it when it runs. An alarm cancelled with its interrupt pending was
never popped, so every later alarm interrupt found the cancelled one at
the front and was dropped: no alarm ran again. Deleting a vtimer erased
its id instead, leaving the last pending interrupt with an empty list.
Both now pop a timer that's gone when its interrupt comes, as
threads/alarm/pending and threads/vtimers/pending show the PSP still runs
the rest, in order.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A broken savestate can have a GE interrupt running with nothing pending,
or a pending entry for an invalid list. Finishing it read past the queue.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
skipDrawReason was saved whole, including the bit that says the host
window is minimized. A state saved while minimized (a rewind snapshot)
kept it set after loading in a normal window, and the other way round.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Which list slot and id the next sceGeListEnQueue gets wasn't saved, so
after a load it depended on the session before it, and replays and
rewinds diverged. Older states start it at 0, and also get the default
EDRAM translation and visible matrices they don't carry, rather than the
previous session's.
- A frame dump recording during a load replayed the loaded state's
commands on the registers from before it. It now starts over from the
loaded state.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>