Most homebrew ME code waits in a loop like lw t0, flag; beq t0, zero,
loop; nop: Daedalus/MElib/NooDS's mediaengine.prx, snes9xTYL, PMP Mod,
Yabause, FBA, the Touhou ports (with a few nops in it). Such a loop stores
nothing, has only plain loads, integer ALU ops, nops and branches, and
writes every register it reads before reading it in a pass, so every
pass goes the same way while memory stays the same - which it does while
the ME runs, since the main CPU doesn't. At the top of one, between
parts of a slice, the ME interprets a pass, and if it comes back to the
top, waits out the rest of the slice.
Loops that also store (a heartbeat counter) or span several blocks
aren't recognized, nor are delay loops counting down a register.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
width/height is volatile and influenced by the framebuffer size detection
of the latest draw so chck bufferHeight instead.
The old check broke the homebrew OOT port.
MIPS.h includes MemMap.h for the address space's definition. Of the
files that include MIPS.h, nearly all already got MemMap.h another way.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The native dispatcher generated for the ME leaves when its downcount
runs out instead of calling CoreTiming::Advance, and IRNativeJit gets
RunDowncount. Pointerify is decided from the CPU's own memory base. ARM32,
RISC-V and LoongArch64 still give the ME the IR interpreter.
LoadKernel/StoreKernel are now compiled on ARM64 and x64 rather than
going through the IR interpreter one op at a time: memory directly, or a
call to fixed code (which saves the caller-saved registers guest values
live in) for 0xBC000000-0xBFFFFFFF. ARM64's block size estimate counts
them as such. The 16-bit ones stay generic while the software renderer's
depth mirrors are active, as Load16/Store16 do. That matters for the main
CPU's kernel code too, and for all code while the ME is emulated, which
runs in kernel mode everywhere. mcidclan's me-soft-interrupt went from
476 billion to 6.4 billion host instructions for 3 emulated seconds,
ending with the same ME counter as the IR interpreter.
The ME unit tests run on the native JIT too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Unless the interpreter is selected, the ME gets its own IRInterpreterJit,
with its own block shadow, and runs it without CoreTiming
(RunDowncount, split out of the dispatcher loop). Its MIPSState never
touches the main CPU's replacement hook.
The IR frontend compiles through the CPU's address space: instructions
read as they are in the ME's memory (no main CPU replacements or hooks),
kernel mode always, jump targets and block bookkeeping checked against
its rules, syscall left to the interpreter (which traps it). An
interpreted instruction that ends a block now gets its own pc first, so
halt goes on from the right place.
The main CPU's icache invalidations are passed on to the ME, which runs
code from the same RAM.
The unit tests run the ME on both cores. mcidclan's me-soft-interrupt
runs about four times as fast.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sysreg (0xBC100000) gets the ME's reset bit (0x4C bit 2), the soft
interrupt (0x44, which interrupts the other CPU: the ME's controller from
the main CPU, PSP_MECODEC_INTR from the ME), the hardware mutex (0x48,
each CPU sets its own bit only while the other's is clear) and the clock
and config registers it writes. The ME's interrupt controller (0xBC300000,
write-1-to-clear, mask at 8) answers the ME only. All of it is the ME's
state and stays unhandled without an ME.
On the ME: interrupts to EBase with EPC and EXL, halt sleeping until an
unmasked interrupt, and Count, which doesn't run: it stays 0 across 10ms
with ERL set after reset and with Status cleared, and reads back what was
written. Reset keeps the GPRs and sets Status ERL/BEV/SR, which homebrew
relies on: mcidclan's samples run C on the stack Sony's ME code left. The
values are from a kernel probe on a PSP-1000 (sp 0x801FFFE0, COP0 22 = 1,
Status 0x20500404, mfic 0, which doesn't gate interrupts).
The main CPU runs base interrupt handlers the way interruptman calls them
(a0 intno, a1 arg, a2 interrupted pc; -2 disables), and interrupt 31 has
a stand-in for me_wrapper's handler, which homebrew releases first.
While the ME is emulated, any main CPU code may access hardware registers:
homebrew calls its own code in kernel mode through a kcall module. This
is a stopgap (g_kernelModeAnywhere, marked HACK): kernel mode should be
part of the CPU's state, switched on while a kernel module's export runs.
The memcpy/memmove replacements copy through MMIO when an end is a
register address, for code copied to the boot page.
mcidclan's me-soft-interrupt sample now runs, counting each interrupt in
both directions.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MIPSState::IsMediaEngine() forwards to its address space, so callers
don't have to reach through mips->space for it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
get-debloated-pkgs.sh hits the GitHub API unauthenticated, so on shared
runner IPs it gets 403 rate limit, sleeps 10 minutes and fails. It uses
GITHUB_TOKEN when set. The pkgforge pacman mirror also drops downloads
now and then, which failed the whole job.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Any JIT block invalidation, such as loading a module, sets
CORE_REENTER_DISPATCH. Core_Break only acted in CORE_RUNNING_CPU, so a
memory exception in that window was logged and dropped on the JITs and
the IR interpreter, while the interpreter (which never asks to re-enter)
stopped. That's how modules/cutrel failed only with --cpu=interpreter.
Stepping supersedes the re-entry request, so break there too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A module cut off before its relocations still has link-time addresses
in its module info. Reading its export table through them raised a
memory exception, which stops the interpreter (the JITs carry on), so
modules/cutrel failed with --cpu=interpreter. Check the range first, as
the import side already does for libstub.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PMDVIS ships a kcall.prx that ends where its relocation sections begin.
On a PSP-1000 (6.61) such a module loads unrelocated if syslib is its only
export library, but with another library the load hangs the PSP
(pspautotests modules/cutrel and modules/cutrel/kernel, which leave that
case out). We loaded it unrelocated, and PMDVIS then crashed on its
garbage export table. Now the load fails with 80020148, the error
PSPLink's modload gave for that file, and PMDVIS carries on without the
ME.
Also ModuleMgrForKernel's 6.61 NID for sceKernelUnloadModule, which the
kernel test uses.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--press=SECONDS:MASK[:DURATION],... holds sceCtrl buttons at emulated
times, and --screenshot-at=SECONDS:FILE,... saves screenshots along the way,
to drive a game past its menus in a run. With presses, the run loop steps
1/60 of a second at a time so short ones land.
ThreadManForKernel's sceKernelPollEventFlag, imported by Yabause-PSP's
me.prx, and sceCtrl_driver's 3.71-3.73 NIDs for sceCtrlSetSamplingMode
and sceCtrlSetSamplingCycle (at the same addresses as the user exports
in ctrl.prx), imported by LightMP3's support.prx.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The firmware's semawm.prx (6.61), which PMP Mod and psp-me-bench load, decrypts
code that walks LoadCore's registered-library hash buckets at the start of this
block, looking for libraries a custom firmware would add. We don't keep those
structures, so it gets an empty table in kernel memory, made on first use and
kept in the savestate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The state that load undo kept was snapshotted before every slot load anyway.
Take it before every load, and if the load fails partway, load it back rather
than resetting the game. Load undo still gets it, now written after the load,
and not when the load was rolled back, so the undo on disk stays the useful one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sceMp3ReserveMp3Handle(0) gives a context with no buffers, which the PCM
buffer size check added to AuCtx::DoState rejected, failing the load partway
through.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The restart shut the GPU down after PrepareCopyDisplayToOutput had bound its
pipelines in the open backbuffer pass, which trips the pipelinesToCheck_
assert in VulkanRenderManager::CheckNothingPending. The failed load already
stops the frame from running, so nothing is lost by restarting first.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Find4GBBase found 256 MB of free address space for the views and then
unmapped it, so the gaps between the views went back to the system. On a
32-bit Android device, a running game had malloc heaps between VRAM and RAM
and GPU driver buffers just past RAM, and masked guest addresses (bits 30-31
cleared) reach a whole GB above base, mostly heap. A stray guest access
landed in those silently instead of faulting, and another thread could even
map something where a view was about to go, which the view's MAP_FIXED then
replaced. That fits the 32-bit-only crashes deep in system libraries and in
our own heap structures.
Keep the range reserved as PROT_NONE and map the views over it, putting the
reservation back when a view is released. Mask out bits 28-29 too on 32-bit
(no valid address has them), so accesses stay inside the reservation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The framebuffer manager only switches between buffered and non-buffered
rendering on a render resize. Turning on Auto FrameSkip clears Skip Buffer
Effects without posting one, so after resuming, EmuScreen ran the buffered
path with a GPU still in non-buffered mode. A frame displayed from RAM was
then drawn straight to output with no render pass bound, crashing in
VulkanRenderManager::SetScissor. Treat a mismatch as a resize in
BeginHostFrame, whatever changed the setting.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ClipTrianglesAgainstNearPlane took limits for the transformed vertices and
the output indices but never checked them, and the caller passed constants
(one of them a typo, 65336). Each partially clipped triangle adds two
vertices, so a large draw with many triangles crossing the near plane ran
off the end of transformed_ and crashed. Pass the real limits, and drop
triangles that don't fit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MeCore owns a second MIPSState in the ME's address space, held in reset
until released (SetReset). It runs on the interpreter, a slice behind the
main CPU: CoreTiming runs it for the cycles the main CPU has just used, in
Advance and ForceCheck. Its own PRId (COP0 22) and mfic/mtic flag; MMIO
counts as kernel mode on it, and a syscall stops it with an error rather
than reaching HLE. The interpreter's block stepping reads delay slots
through the stepping CPU's memory, and cache instructions log at verbose
level, as ME code flushes its caches in a loop.
Created at boot only for homebrew (PBP with MG category or no SFO, ELF;
the VSH counts, as vshmain.prx loads as an ELF), when "Enable ME
emulation for homebrew" is on: a global checkbox in developer settings,
off by default and not translated yet, or --me-core. Saved in its own
section after MMIO (SaveStart v5), with its own memory; loading a state
creates or removes the ME to match.
The boot page now starts with a reset stub that sends the ME on to
0xBFC00040, where homebrew puts its code (from the firmware's reset code,
read on a PSP-1000).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The ME sees the main CPU's RAM, but has its own eDRAM at 0 (with
0x80000000 and 0xA0000000 mirrors) where the main CPU has its scratchpad,
its own memory where the main CPU has VRAM, and both CPUs share the boot
page at 0xBFC00000.
- The ME's own memory is views in the main arena flagged MV_ME_ONLY, which
only Memory::CreateMESpace() maps, into a second reservation along with
the shared views at their same arena positions. It gets its own block
shadow.
- The eDRAM (MV_EDRAM) is the ME's half of the eDRAM chip, the GE's VRAM
being the other: 2MB on the PSP-1000 (whose resident ME image says so),
4MB on the others (from mcidclan's libmecore, not hardware-checked).
- At VRAM's addresses, through 0x44000000 and 0xA4000000 alike, the ME
doesn't see VRAM but memory of its own (pspautotest me/vram, on a
PSP-1000): where homebrew keeps the VME's buffers. Its size isn't known;
2MB with VRAM's mirrors here, and no depth layout on 16-bit accesses.
- 0xA0000000 reaches physical memory uncached, so RAM is mapped at
0xA8000000 too.
- The boot page is a view too, which the main CPU reaches through a new
MMIO device (in savestates from MMIO section version 2). The ME runs
code from it, uncached, so the main CPU's writes invalidate code there.
On masked platforms it isn't mapped, and the device uses a plain
buffer.
- AddressSpace has IsValidAddress etc, with the ME's rules when edramSize
is set, and the interpreters and the IR interpreter check and fetch
instructions through their state's space. The IR compiler's checks still
use the main CPU's, until the ME gets a JIT.
- On Darwin, MemArena can hold several view reservations.
Masked platforms (iOS, 32-bit) don't get an ME space yet.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
What the ME sees at VRAM's addresses, recorded on a PSP-1000. In the
next set: it needs the ME on, and Sony's ME firmware to hand the ME back
to at the end.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Save8888RGBAScreenshot() calls png_image_write_to_memory() with the buffer
it's given, or with none when it's empty, which only measures the PNG. That
call succeeds too, so it returned a buffer of the right size, all zeros. Its
only caller so far passes one that's big enough.
GetSubFile() reads a whole subfile, and refuses those over 256MB.
ReadSubFile() reads a range of one, for the PSAR of a PS1 EBOOT, which holds
its disc image.
ioctl 0x04100002 (set the PGD offset) gets a pointer to the offset with
inlen 4 from the firmware's POPS manager. Our own sceNpDrmEdataSetupKey still
passes the offset itself.
When ioctl 0x04100001 finds no PGD there, the file is read as plain data from
that offset, not from the start (the same for ordinary files, whose offset is
0). A converted PS1 EBOOT's disc image is plain, and POPS reads it right
after. A PGD with the wrong key still leaves the file at 0.
The firmware's POPS manager imports these by their 6.60 NIDs:
sceKernelLoadModuleForKernel and sceKernelStartModule from ModuleMgrForKernel,
SysMem's sceKernelMemset and sceKernelGetUIDcontrolBlock, Sysclib's strnlen,
and sceKernelExtendKernelStack, which runs the callback like
sceKernelExtendThreadStack does.
sceKernelGetUIDcontrolBlock reports no such UID: the manager uses it to make
a descriptor it opened in kernel mode usable from user mode, and ours aren't
access-checked.
COP0's control registers, which the firmware's exception code (and
ProTrekkr's Media Engine stub, clearing $14 and $15) keeps state in: 32
words next to cop0 in MIPSState, saved from section version 6.
Interpreted, as the other COP0 instructions are.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sceKernelIcacheInvalidateAll invalidates 0..0x3FFFFFFF, which is a
million 1 KB pages, each looked up in the hash map. A homebrew calling
it once per frame spent most of its time there. When the range has more
pages than the table has entries, go through the table instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ExceptionManagerForKernel's table was never registered, so kernel
modules importing sceKernelRegisterDefaultExceptionHandler (LightMP3's
exception.prx) were left with an unresolved import. It now holds only
that one: registering the unimplemented entries would take them from
the firmware's exceptionman.prx under --vsh. Nothing could have saved
the old layout, since it was never registered.
sceCtrl_driver's sceCtrlSetButtonIntercept hides buttons from user mode
readers, which Daedalus does with the system buttons games don't get
here anyway: a no-op. Daedalus's Media Engine build needs both.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Custom firmware calls, which homebrew that runs ME code uses to reach
its own kernel modules: KUBridge's kuKernelCall runs a function with the
first eight of its arguments and stores v0/v1 in ret1/ret2 afterwards;
sctrlHENFindFunction (SystemCtrlForKernel, and a new SystemCtrlForUser)
finds a loaded module's export by module, library and NID.
Tanaka's Touhou ports find and call their GE4 wrapper's exports with
them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The calls a PSP-2000 and later switch to their 4MB of VRAM with. We have
the PSP-1000's 2MB, and answer as it does, recorded in the new
gpu/ge/kernel/edramsize. Tanaka's Touhou ports import them in their GE4
wrapper module, and fall back to 2MB.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
User modules stay at 0x08-0x77. Recorded on a PSP-1000 in the new
threads/threads/kernel/priority. ProTrekkr's ME test, a kernel module,
creates its audio thread at priority 2.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The interpreter does nothing for any cache op but icache invalidation, but
the IR only knew four of them and sent the rest to the interpreter one at
a time. Media Engine code flushes its data cache with a loop of 0x14s,
often every pass of its main loop: with the ME emulated (on a branch),
mcidclan's me-simple-count went from 15.9 to 1.0 seconds for 3 emulated
ones.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
They keep the PSP from suspending while held, which never happens here.
mcidclan's vme-primary-dmac and PMP Mod call them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Memory::AddressSpace is where a CPU's addresses point (base) and its block
shadow, with the unchecked accessors and block shadow helpers as methods. The
main CPU's is Memory::g_mainSpace, kept in step with Memory::base and
blockShadow, and MIPSState::space points at it.
The CPU cores now go through their state's space instead of the globals: the
interpreter's and IR interpreter's loads and stores (which also saves the IR
interpreter reloading the global base), the IR interpreter's block lookup,
the block table's shadow entries, and the native JITs' memory base register,
block shadow lookups and pointerified registers. Address validity and
everything outside the cores still use the globals.
No change for the main CPU; this is so another CPU (the Media Engine) can see
memory differently. Benchmarked against master (God of War: Chains of
Olympus, Tekken 6): JIT instructions retired within noise, IR interpreter
2% fewer.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>