Commit Graph
15489 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5 ba7bf86e73 sceAudiocodec: don't return a zero Atrac3+ frame size for odd bitrates
CalculateInputBytesAndChannelsAt3Plus only set the frame size for the four
bitrates it had a table entry for and left it at 0 otherwise, which fails the
decode - the same shape of bug the AAC path just had. formatByte2 * 8 + 8 is
the size for all four known bitrates, so use it for the rest too; the firmware
never returns 0 here. The common PSMF path is unaffected: it takes the size
from the frame's own header before reaching this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 10:45:16 -06:00
Henrik RydgårdandClaude Opus 5 b561de3ff1 sceAudiocodec: size the AAC input frame like the firmware
The AAC decode path read the frame size from srcBytesRead, which is an output
field the decoder writes - it's 0 on the first call, so the decoder was handed
0 readable bytes and audio never started. avcodec.prx's decodeUtility sizes the
AAC input from the byte at 0x2c instead: 0x609 when nonzero, 0x600 when zero.

Investigated after report by sum2012 in #16238. Not actually tested yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 10:33:08 -06:00
Henrik RydgårdandClaude Opus 5 45727ef2be Tighten the comments on the mpeg PRX changes
Cut restatement and asides that only made sense against earlier, wrong versions
of the code, and prefer parentheses over paired dashes. Also fix two comments
left stale by the descriptor rework, and record the rule in AGENTS.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 10:09:48 -06:00
Henrik RydgårdandClaude Opus 5 e784f8bccb sceMpegbase: delay the colour conversion, as the hardware does
sceMpegBaseCscAvc/CscAvcRange run on the DMACPLUS and take real time. A
psmfplayer game re-blits the current video frame every render frame while it
waits for the next, so an instant return here is a tight loop that never yields
and starves the audio thread the playback clock is paced by - the whole A/V
pipeline then deadlocks a few frames into the movie. SOCOM: Tactical Strike
hung exactly this way running the real mpeg.prx; with the delay it plays. Same
value and reason as our sceMpeg HLE's sceMpegAvcCsc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 09:53:03 -06:00
Henrik RydgårdandClaude Opus 5 4079adf424 sceVideocodec/sceMpegbase: fix the Media Engine frame descriptor
mpeg.prx reads the eight frame buffer addresses straight off the front of the
structure sceVideocodec publishes - `lw` at 0x00..0x1C, the same in Daxter's
disc copy (1.3, at 08805698) and in flash0:/kd/mpeg.prx (1.8, at 08805898) -
and takes the dimensions from its own context. We were writing the dimensions
at 0x00/0x04 and the buffers at 0x10, so slots 0 and 1 received 17 and 30 and
the four chroma addresses never arrived at all.

That survived in Daxter only by cancelling out: mpeg.prx hands the same words
back in the descriptor it builds for the colour conversion, which read them
with the same skew. It bites as soon as they are used as real addresses.

So also:

- sceMpegBaseYCrCbCopy moves the frame, rather than copying 48 bytes of
  descriptor over the caller's table of destination pointers. mpegbase.prx
  builds a DMA list over the eight buffers (080010f8 in mpegbase_260.prx):
  flags bit 0 takes 0,1,4,5 and bit 1 takes 2,3,6,7. mpeg.prx always passes 3.
- The chroma buffers are paired like the luma ones, left/right then even/odd
  rows, which is what that flag split assumes. Ours grouped them by half, so
  the per-buffer sizes disagreed with the caller's.
- The conversion reads the descriptor as mpegbase.prx does, and takes the
  buffers from wherever they are: still in the Media Engine, or already copied
  into the game's memory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 21:32:36 -06:00
Henrik RydgårdandClaude Opus 5 992b8bd9b6 sceMpegbase: tell the GPU that the colour conversion wrote a frame
The CSC writes RGB straight into the display buffer, which the hardware
backends can't see on their own - our sceMpegAvcCsc HLE calls
PerformWriteFormattedFromMemory for exactly this reason, and the mpegbase
path didn't. Daxter's intro decoded normally with the screen frozen on the
menu behind it. Invisible under --graphics=software, which reads that memory
directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 11:06:56 -06:00
Henrik RydgårdandClaude Opus 5 16bf3a6519 BlockAllocator: let an allocator with no blocks save and load
An allocator that nothing has Init'd yet is a real state, not a broken one -
sceVideocodec keeps one for Media Engine memory that stays empty until a game
plays a video - but DoState asserted on bottom_ when writing, and on reading
treated a block count of zero as corrupt. Both ends handle it now, and the
write loop no longer special-cases the first block.

The stream layout is unchanged, so states written before this still load: they
always had at least one block, and take the same path they always did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 11:06:56 -06:00
Henrik Rydgård 03bb3e20fe Merge pull request #22286 from hrydgard/real-mpeg-prx
Implement sceVideocodec, run sceMpeg as LLE on top (controlled by DisableHLE)
2026-09-14 17:23:01 -06:00
Henrik Rydgård 9e7f94272b Merge pull request #22288 from hrydgard/debugger-fixes
Win32 debugger: stop cutting off register values in CtrlRegisterList
2026-09-14 17:22:46 -06:00
Henrik Rydgård 10a9d7bb31 De-claude some overly verbose comments 2026-09-14 10:42:55 -06:00
Henrik RydgårdandClaude Opus 5 6600d1c05f Savestate browser: delete the screenshot and name file along with the state
Deleting a savestate from the savedata screen goes through GameInfo::Delete,
not SaveState::DeleteSlot, and it only knew about the .jpg - so the slot's
.name.txt was left behind with nothing to belong to.

Rather than teach the UI the naming scheme a third time, SaveState now answers
what sits beside a state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 13:58:20 -06:00
Henrik RydgårdandClaude Opus 5 55f53d4523 Savestate names: read from the cached listing, and tidy up the name files
GetSlotCustomName hit the disk for every slot each time the pause screen built
its views, almost always for a file that isn't there. Rescan's listing already
knows, and HasSaveInSlot - which gates whether the name is even shown - reads
the same map, so this can't hide a name the old code would have found.

SetSlotCustomName now rescans, so a rename is visible without depending on the
pause screen happening to rescan on its way back.

Also: clearing a name deletes the file instead of leaving an empty one behind,
and the extension is "name.txt" rather than plain "txt", so an unrelated
"<prefix>_<slot>.txt" in the savestate folder isn't read as a slot name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 13:58:20 -06:00
lain 78a00399ab Allow to define and modify custom names for saved states 2026-09-12 13:58:20 -06:00
Henrik Rydgård c81bc5c212 Win32 debugger: stop cutting off register values in CtrlRegisterList
The name and value columns were hardcoded at x=17 and x=77. The control has
a fixed width in the dialog and can not be widened, so the 8 hex digits of a
GPR only just fit - and once the register list got a vertical scrollbar, the
~17px it takes off the client width pushed the last digits off the edge.

Derive the value column from the client width each paint instead, pulling it
in far enough for a whole value to fit and clipping anything longer rather
than letting it spill. Same for the category tab labels.

Float registers print with %g rather than %f: in a column that narrow, a
clipped %f of a large value is worse than useless (1e20 would read as
100000000), while %g keeps six significant digits and stays short for the
values you normally see.
2026-09-12 13:46:12 -06:00
Henrik RydgårdandClaude Opus 5 6110a27b73 sceVideocodec: one layout helper for the eight frame buffers
Four places were each computing the same sizes and 64-byte-aligned offsets:
the allocation, the recovery sceMpegbase uses, and the readers on both sides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 9422cb4899 sceMpegbase: save its state
The output pixel mode decides both the colour packing and the bytes per pixel of
the conversion, and a game sets it once per movie rather than per frame - so a
state resumed mid-movie converted at the default until the next
sceMpegBaseCscInit, which may never come. The gathered PES payloads go in too,
since a state can land between the copy and the decode that consumes it.

The section is optional (minimum version 0), so states written before it still
load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 b4b8e2e733 AvcDecoder: Check the pixel format
AvcDecoder handed out frame_->data[0..2] without looking at the pixel format,
and everything downstream indexes them as 8-bit 4:2:0. Check it, and drop the
frame instead of converting whatever else turned up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 435d446c37 sceVideocodec/sceMpegbase: stop holding on to contexts and payloads that are done
Decode looked its context up with operator[], so a game that never opened one
would still get an entry and a decoder, and only Delete ever removes those.
Require the context to exist instead.

The gathered PES payloads were likewise kept for the whole boot, so the first
decode of a movie could be handed the last packet of the previous one if the
address came round again. Hand each payload out once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 779f9eb811 sceAudiocodec: check the whole Atrac3+ frame is readable before decoding it
For a frame that still has its PSMF header, the length comes out of the header
itself, so it's only as trustworthy as the stream - up to 2816 bytes read from a
pointer where only the first four had been checked. Validate the span, and use
IsValidRange rather than a range accessor, which raises a memory exception
instead of returning null.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 9dfce3ec2f ImDebugger: list the sceVideocodec contexts alongside sceAudiocodec
Same shape as the audio table: one row per open context, with its EDRAM block
and frame buffer allocation. Both are in ME memory, so the addresses shown are
in that space, not in PSP RAM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 ad7cfefd49 sceMpegbase/sceAudiocodec: make audio work with the real mpeg.prx
Three bugs stacked on top of each other, all of them silent - the decoder
happily returned 2048 samples of digital silence 878 times per movie.

sceMpegBasePESpacketCopy gathered the scatter-gather blocks but never performed
the copy. That was fine for video, whose destination is Media Engine memory we
can't write anyway and which we intercept instead, but audio is copied into
main memory and mpeg.prx then hands that same address to sceAudiocodecDecode as
its input. It was reading a buffer nothing had written.

With real data arriving, the Atrac3+ frames turned out to still carry their
8-byte PSMF header - libatrac3plus.prx strips it before calling us, mpeg.prx
leaves it on for the hardware to parse. Detect the 0x0FD0 sync word and step
over it, taking the frame size from the header the way MpegDemux already does
on the HLE path.

Finally, rebuild the decoder if the frame size only becomes known at the first
decode: the context has no size in it at init time, and a decoder built for a
zero-byte frame decodes nothing.

Also corrects a comment claiming mpeg.prx passes zeroed Atrac3+ format bytes.
It doesn't - they were zero because of the missing copy above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 9c4173fe51 sceAudiocodec: drop a format check CheckNeedMem doesn't actually do
avcodec.prx's sceAudiocodecCheckNeedMem validates the codec id and the pointer,
sets the magic and forwards to the ME. It never looks at the format bytes, and
the caller isn't obliged to have filled them in yet - the real mpeg.prx calls it
with both still zero, and our invented check stopped its audio dead with
SCE_AVCODEC_ERROR_INVALID_DATA.

Now just a warning when they aren't the pair libatrac3plus writes, which is
still worth knowing about.

Found by running the real mpeg.prx against our sceAudiocodec, which is exactly
what that reference is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 00770764a7 Run the real mpeg.prx in place of our sceMpeg HLE
Add flash0:/kd/mpeg.prx to the sceUtility module swap, hung off av_mpegbase,
so it loads when DisableHLEFlags::sceMpeg is set. Games that ship their own
sceMpeg_library on the disc never get here and just use theirs.

The piece that was missing to make it work: the access unit address mpeg.prx
passes to sceVideocodecDecode is in Media Engine space, which we can't read.
On hardware sceMpegBasePESpacketCopy DMA'd the payload there first. That copy
is ours, so it now gathers the blocks and sceVideocodec decodes from those,
falling back to main memory for any caller that points at it directly. The
gather is keyed by destination, because that call carries the audio payload
too - video to an ME address, audio to main memory - and handing an ATRAC
packet to the H.264 decoder gets you nothing.

With that, video/mpeg/basic gets a 144x80 frame out of real Sony code driving
our sceVideocodec through ffmpeg. The test still fails overall, as it did
before this branch - it is in tests_next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 ba975d2908 sceMpegbase: recover the chroma buffers the descriptor doesn't carry
mpeg.prx copies only the four luma buffers into the descriptor it passes to
sceMpegBaseCscAvc, at +0x20, and keeps a stride where the chroma ones would
be - so the conversion had nothing to read. Both ends of this are ours, so the
full set is recovered from the allocation sceVideocodec handed out.

Death Jr. now runs the whole chain: 504 frames decoded by real mpeg.prx through
sceVideocodec, and 505 colour conversions into the game's display buffers at
480x272, no exceptions.

The picture is still black, and the cause is now upstream of all of this: the
decoder itself returns blank frames, so what we feed it isn't the video
elementary stream. The first access unit being 162 bytes was the early sign.
sceMpegBasePESpacketCopy gathers the LLI blocks it is given, and that is
evidently not the whole story - PES headers, or blocks it isn't counting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 184c2cb4ea Implement sceVideocodec, the ME's H.264 decoding interface
The last piece mpeg.prx needs from us. It decodes through AvcDecoder and writes
the result where sceMpegbase expects to read it: for type 0, the eight-buffer
tiled layout, produced here as the exact inverse of the reader added with the
colour conversion. Type 1 hands back plain planar YUV instead.

The descriptor mpeg.prx passes in is empty - on hardware the Media Engine owns
the frame buffers and reports back where it put them - so they are allocated
here and their addresses filled in, and sceMpegbase can recover the full set
from that allocation afterwards. The list is only read as addresses once it
holds addresses: a caller that leaves small integers there used to take us
straight into a memory exception.

Both those buffers and the block sceVideocodecGetEDRAM hands out live in a
model of the ME's own 2MB of embedded DRAM rather than in either PSP partition,
with a BlockAllocator carving it up. The CPU can't reach ME memory on hardware:
mpeg.prx keeps the EDRAM value and hands it back without ever dereferencing it,
and it has no way to say where the frame buffers should go - sceVideocodecSetMemory
is given a frame size and a buffer count, 480, 272 and 2 for a full-screen movie.
Taking either from the game would push its own allocations around or spend memory
a real PSP never spends, so sceMpegbase reads the frame through a host pointer
into that block instead of through Memory::.

A game can hold several contexts at once - Silent Hill Origins runs two, one
that owns the EDRAM and one that does every decode - so the decoder, the frame
buffers and the EDRAM address are per context, keyed by context address the way
sceAudiocodec keys its decoders. The EDRAM itself the firmware tracks in the
caller's context struct and nowhere else, including refusing a second request
rather than replacing the first, as videocodec_260.prx shows, so we do the same.

Registered at the end of RegisterAllModules, since a savestate stores the
syscall opcode encoding that order.

GetSEI, ScanHeader, GetFrameCrop and the two unnamed NIDs return 0 and log -
mpeg.prx calls them but nothing yet shows what they need to return, and
guessing seemed worse than being loud about it.

Untested: nothing calls this until mpeg.prx is loaded, which is the next step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 3276cdf65f Add AvcDecoder, a plain access-unit-in, frame-out H.264 decoder
sceVideocodec is the interface the Media Engine really exposes: the caller owns
the buffers and hands over one access unit at a time. MediaEngine can't serve
that shape - it is built around sceMpeg's own model, with the PSMF demuxer, the
ringbuffer and its own frame pacing wrapped around the decoder - so this is a
second, separate path rather than a refactor of code every game that plays
video depends on. Merging the two is worth doing once sceVideocodec has earned
its keep.

Nothing calls it yet; sceVideocodec is the next piece.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:53:13 -06:00
Henrik RydgårdandClaude Opus 5 7c6f9d18b8 sceMpegbase: implement the colour conversion functions
Five of the six functions in this module were registered as nullptr, so any
caller reaching them got nothing. mpeg.prx needs all of them, and this is the
first of the pieces required before the real module can run in place of our
sceMpeg HLE.

The module gets its own file at the same time. It was a tail of sceMpeg.cpp,
which is long enough already, and everything below is new code for it.

sceMpegBaseCscAvc and sceMpegBaseCscAvcRange convert the ME's decoded output to
RGB. The 48-byte descriptor holds the frame size in macroblocks at 0x10 and
0x14 - it appears at 0x00/0x04 too, but scaled differently depending on which
path built it, so the second pair is the one every caller agrees on - and the
four luma buffers at 0x20. What they point at is not planar: luma arrives as
16-pixel-wide vertical strips alternating between buffers on every other line,
and chroma as Cb/Cr pairs interleaved across four more. All eight are untangled
into planes once per frame before converting, which keeps the pixel loop
readable.

sceMpegBaseCscInit carries the default buffer width for callers that pass zero.
sceMpegBaseCscSetPixelMode (0x0530BE4E - the official name isn't known) carries
the output format, in mpegbase's own numbering rather than the GE's: mpeg.prx
gets it by indexing a table of {1, 2, 3, 0} with the pixel mode the game gave
sceMpegAvcDecodeMode, making 0 ABGR8888 and 2 ABGR5551. Read as a GE mode it
turned Thrillville's black into blue.

sceMpegBaseYCrCbCopy copies the descriptor but not the buffers it points at,
and says so in its log - nothing seen so far needs more, and inventing the
buffer copy without something to test it against seemed worse than a note.

Behaviour is cross-checked against JPCSP, which implements all of these.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 11:52:09 -06:00
Henrik RydgårdandClaude Opus 5 4df01fd990 sceUtility: load the firmware's module for a disabled HLE library
Doing this was written once, for sceMp4, remembering the two UIDs it had
loaded in a static. That static was the only thing stopping a second copy
being loaded on top of the first, and it didn't survive a savestate: resuming
in a fresh process left it at zero while the restored module list already had
the modules in it. Ask the module list instead, which needs no state of its
own and is right after a savestate load and across games.

With the mechanism shared, sceMp3 and sceAtrac get it too. libmp3.prx and
libatrac3plus.prx each import nothing but the kernel and sceAudiocodec, so
both run against what we already have. The sceAtrac checkbox previously led
to an error log and a debug assert saying it wouldn't work, with a note that
we could go and find the file - which is what this does.

This is only ever the path for a game that ships no copy of the library. One
that does loads it directly and never asks sceUtility for it.

Also:
 - a module we're really loading no longer reserves its stand-in block as
   well. That block only stands in for memory we aren't otherwise taking, so
   reserving both charges the game twice, at the top of user memory where
   thread stacks come from.
 - the module loads at the bottom of the user partition. fromTop is for a
   module injected before the game's executable is placed; by the time a game
   calls sceUtility nothing moves either way, and the firmware's utility.prx
   allocates AV module memory as PSP_SMEM_Low.
 - NotifyLoadStatusAtrac read the raw setting rather than the effective
   flags, so it could fire for a flag that wasn't in effect.
 - the missing-firmware case now says so on screen, naming the library and
   pointing at installing a firmware, rather than only logging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 11:11:41 -06:00
Henrik Rydgård b693490ced De-claude some comments 2026-09-11 21:24:18 -06:00
Henrik RydgårdandClaude Opus 5 50d00541f9 headless: run graduated modules for real on a disc, and add --firmware-from-disc
Headless force-enabled HLE for everything the caller didn't name, so a run never
exercised the graduated modules the way the app does. That is right for
pspautotests, which is homebrew PRXes shipping none of the user libraries a
retail disc carries - scePsmfPlayer and friends would have nothing real to run.
It is wrong for a disc, which brings its own copies. Decide from the resolved
boot list: a test batch, or a --vsh run with no file at all, keeps the homebrew
treatment; exactly one non-executable target is a disc and is left alone.

--firmware-from-disc installs the firmware most discs carry into a scratch NAND
beside the memstick and boots against it, which is the version that game shipped
with. Kept per disc so a second run reuses it. It goes after the point where the
NAND root is settled, since that assignment is unconditional and would otherwise
overwrite it - which it silently did until the sceFont check started depending
on the answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 21:09:49 -06:00
Henrik RydgårdandClaude Opus 5 039ffc682d HLE: graduate the leaf libraries games carry on the disc
A survey of disc images found these shipped on a lot of them: LIBDEFLT and
LIBMT19937, LIBADLER, LIBMD5 on about 1/6, LIBSHA256 on 1/7, LIBHEAP on
1/12, LIBSFMT19937 on 1/24. None appears in any firmware dump and none is loadable
through sceUtility, so a game that imports one has to carry it - which makes
running the game's own module the safe default, the same argument psmfplayer
graduated on. Read off real discs with --re-module disc0:/..., all but sceHeap
import nothing at all, and sceHeap only Kernel_Library and ThreadManForUser.

sceFont joins them with a condition. Its module is on the disc like the others -
the most-shipped of all, on a third of the discs - but it reads its fonts from
flash0:/font and has nothing to fall back on, so it is only usable with a
firmware dump installed (our own fallback fonts are just not good enough anyway, it
chokes on them). CheckDisableHLEAvailability puts the HLE back when
those fonts are missing, since ours does have a fallback.

sceParseUri and sceParseHttp look like candidates and aren't: they are in the
firmware and sceUtility can load them, so a game may import them without
shipping one. Their headers say so.

The HLE implementations stay - old savestates index these tables by syscall
opcode - and are marked legacy the way scePsmf is.

Also: Move HLEInit after the filesystems are mounted

It ran before MountFileSystems, so the checks in it deciding whether a library
can run its real module instead of our HLE couldn't ask the PSP filesystem and
had to reach for host paths under the NAND directory. Nothing between
CoreTiming::Init and here touches HLE, and the kernel and the game's modules are
both loaded later, so it can simply move.

Two things fall out. The checks now spell their paths flash0:/... like the rest
of the emulator. And they run after AutoInstallFirmwareFromDisc, so a disc that
carries a firmware counts on the launch that installs it rather than the next
one - verified with an empty NAND and a disc carrying 3.11: the fonts land and
sceFont runs the game's own module the same boot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 21:09:33 -06:00
Henrik RydgårdandClaude Opus 5 9a77ebb743 sceAudio: tighten up the rest of the savestate handling
A re-read of the whole thing after the reservation bug, looking for anything else
it got wrong.

The SRC channel's buffer count is read straight out of the state and then used to
index a two-element array, so a corrupt one is a write outside the struct. It is
range-checked now, the same way the channel count above it already was.

A channel holding a buffer with no samples left of it was stuck: the mixer skipped
it without ever clearing the address, so it read as busy for ever and the game had
no way back to sound. The API cannot produce that - a channel is never reserved
for zero samples - but a savestate can claim it, so the mixer now retires such a
channel instead of stepping over it.

Also took the "get rid of this next time we bump" the version-2 resampler section
came with, since this branch is that bump. Nothing was ever in it.

Checked by loading states written by a released build for a game that uses the
mixer channels and one that uses Output2, plus a round trip of the new format.
Every section ends in a marker, so a conversion path that consumed the wrong
number of bytes would fail the load rather than quietly corrupt what follows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 c1bbd6ae2b sceAudio: stop dropping the reservation when loading an older savestate
Loading a state written by a released build left every channel unreserved, so
the game's next output came back SCE_ERROR_AUDIO_CHANNEL_NOT_INIT and the audio
never recovered.

The old format's reserved flag, sample count, volumes and format are read at the
top of AudioChannel::DoState and are perfectly good. Only the play position
cannot be reconstructed. The conversion path used clear() to zero the fields that
had no equivalent, which threw the rest away with them - my own cleanup two
commits ago swapped explicit field assignments for that call and did not notice.

The SRC channel had the same problem from the other end: older states carry it as
a ninth entry in the channel array, and that record was read into a throwaway.
What reserve agreed on carries over fine, so it now does.

Both halves were checked by loading a state written by a master build, with and
without the fix. Newer states are untouched and still round-trip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 84bd8459e9 sceAudio: implement sceAudioOneshotOutput, and cover the rest of the channel rules
Another pass looking for gaps, all of it now recorded by audio/blocking/channels
and audio/blocking/oneshot.

sceAudioOneshotOutput was the last unimplemented entry in the module, returning
"library not linked" to anyone who called it. It plays one buffer on a channel it
never reserves, so the channel frees itself when the buffer runs out, and its
argument checks are their own set: any positive sample count, aligned or not, no
upper bound, a negative volume rejected rather than skipped, and no busy check at
all. No game is known to use it; it is implemented because tracing it turned out
to be cheap, not because anything needed it.

The channels test confirms four behaviors: sceAudioChReserve(-1) skips a released
channel that is still playing while a reserve of it succeeds, a second channel
joining a running mixer doesn't lose a block unlike a first, mono counts down in
the same 64-sample steps over the same time as stereo, and the panned blocking
output has no extra delay on the high channels.

Also: the SRC resampler now interpolates into the next buffer at a buffer join
instead of holding the last sample, since the codec reads the two descriptors as
one stream. That only shows up at non-native rates and no test can see it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 412d11534c sceAudio: drain on vaudio release, and charge what an output call really costs
Two things the last pass looked at and left, now measured on hardware by the new
audio/blocking/vaudio and audio/blocking/overhead.

sceVaudioChRelease is not shaped like the Output2 and SRC releases. It hands the
channel a null pointer first, which waits for a buffer to finish, so it blocks for
one buffer and returns 0 where the others would refuse - and the buffer is played
out instead of being dropped, which is what we were doing. The reservation goes
now rather than when the drain finishes: the caller is parked either way and gets
the same answer at the same time, and the buffers keep playing because the mixer
does not look at the reservation.

The same test turned up two more: a *failed* sceVaudioChReserve still marks vaudio
reserved, so a caller that lost the channel to Output2 is told 0x80000021 next
time until a release clears it; and sceVaudioChRelease ignores that flag entirely
and releases whatever holds the SRC channel, which pspautotests already called the
"wrong release".

The flat 10000 cycles charged to every Output2 and SRC output turns out to be
right only for the refused case. Every other outcome ends up querying the codec
and costs over 100us on hardware, including finding the channel unreserved, so
those now cost 25000. Mixer channels are the other way round - cheap to refuse,
expensive on the one output that starts the DMA - so that charge moved to the DMA
start. F1 2009 behaves identically and Burnout Dominator's mixed output is
bit-identical to before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 000af25d81 sceAudio: split the SRC channel out, and stop rescheduling mid-syscall
The nine channels were one array of one struct, but only index 8 ever used the
two DMA descriptors, the played/fraction position and the waiting-thread vector,
and only 0-7 ever used the buffer slot - so eight copies of a std::vector sat
there dead. They are two different pieces of hardware and now they are two
structs: AudioChannel for the eight the mixer walks, and a single AudioSRCChannel
for the one the codec reads directly.

Found while rereading it:

- __AudioUpdate can now run from inside an output call, and it reschedules when
  it wakes a thread. A context switch there lands before the syscall writes its
  return value, so the caller loses it and the woken thread gets it instead.
  Deferred with hleReSchedule when a syscall is in flight. The same fix applies
  to the release path, which had the problem before this branch existed.
- A finished SRC buffer whose waiting thread had given up dropped the completion
  entirely, so the next caller waited a buffer too long. Now it walks past dead
  waiters and banks the completion if nobody is left.
- An output with a null pointer changed the channel volume, where the hardware
  returns before touching it.
- A busy channel came back as an error from the Output2 path and as debug from
  the mixer path. It is an ordinary answer a game polls on, so both are debug
  now; the old behavior filled the log with 18k error lines in a minute of F1
  2009.
- AudioChannel::reset had no callers left: releasing a channel with a thread
  parked on it is refused, so there is nobody to wake.
- The two routing modes are globals and were written once per channel. They get
  their own small savestate block.

Savestate: the sceAudio section goes to 3, and the SRC channel gets a section of
its own. States from released builds carry it as a ninth channel record, which is
read and discarded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:03:07 -06:00
Henrik RydgårdandClaude Opus 5 ac50cd5759 sceAudio: model the real buffering, so contending threads get told "busy"
I deduced that this was the case, and attempted implementing this path long ago,
but I could never quite get it to work in all games. Set Claude on a quest to
research and implement it, and lo and behold, it works. A bit sobering.

Fixes #12888 and likely more. Additionally, audio latency is likely slightly
improved overall, and memory usage is down by 4.6MB.

Claude says:

The blocking output calls are not a queue that callers line up behind. Each
mixer channel holds exactly one buffer and at most one parked thread; a second
thread arriving while the first is waiting is told the channel is busy and is
expected to skip its turn. The Output2/SRC channel holds two DMA descriptors and
refuses a third caller outright, without waiting at all.

We blocked everyone instead, so a game running a movie thread and a sound-effect
thread over one output made the two alternate - a frame of movie audio, a frame
of effects silence - and the movie played at half rate. That is #12888, seen in
F1 2009 and Colin McRae: DiRT 2. With this, the movie thread keeps the channel
for the whole cutscene and the effects thread is refused, which is what the
hardware trace shows.

The driver also never copies a buffer on the way in: it stores the pointer and
its mixer walks it forward 64 samples at a time out of the game's own memory.
Modelling that fixes #20095 as a side effect, and drops the 4.6MB of per-channel
sample rings we were carrying. The mix event is re-phased to the moment a DMA
starts, since the mixer thread outranks its caller and gets a block in before the
output call returns.

Along the way: the two rest-length calls differ after a null-pointer output,
sceAudioChRelease reports not-reserved rather than not-init,
sceAudioChangeChannelConfig validates the format,
sceAudioChangeChannelVolume validates nothing, and sceAudioOutput2ChangeLength
takes a range of 17..4111. Details in docs/sceAudio.md.

Savestates: AudioChannel goes to version 4. Older ones stored mixed samples that
can't become a pointer and a position again, so they load with the pending audio
dropped and any parked threads released.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:03:07 -06:00
Henrik Rydgård 6fa654fce2 Merge pull request #22277 from hrydgard/kernel-mode-partitions
Kernel mode partitions and sceIo behavior
2026-09-10 17:02:50 -06:00
Henrik Rydgård 21fa7f10e1 Minor fixes 2026-09-10 16:04:39 -06:00
Henrik RydgårdandClaude Opus 5 ad06cbe2c4 Auto-install firmware from the game disc on boot
Most UMDs carry a firmware updater, and having a real firmware in the NAND is
what the LLE modules want. On boot, if the disc's updater is newer than what's
installed - or nothing identifiable is installed, which is what a fonts-only
NAND looks like - unpack it, with a progress bar on the OSD. Controlled by
bAutoUpgradeFirmware, on by default, with a checkbox on the firmware screen.
Off in headless, which shouldn't rewrite the NAND during a test run.

The install itself is now shared with the install screen, and hardened, since
it can run without anyone watching:

- It stages into <NAND>/install-staging and only erases the installed firmware
  once the new one is complete on disk, so a failure leaves what's there alone.
- A single entry that didn't unpack fails the whole install. A firmware with
  holes still looks installed, so nothing would ever replace it.
- A truncated archive is rejected instead of unpacking to half a firmware that
  every stat reports as a clean install. The PSAR header records the length;
  it was being clamped to the buffer rather than checked against it.

Also falls back to the version in the archive's own header when the PARAM.SFO
next to the updater is unreadable, which it is on a fair number of discs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 15:52:53 -06:00
Henrik RydgårdandClaude Opus 5 e4d940604e Name both versions in the firmware overwrite warning
"Firmware 6.20 is installed. It will be erased and replaced with 6.60." is the
thing worth double-checking before wiping a firmware - installing off whatever
disc is to hand makes going backwards easy to do by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:52:53 -06:00
Henrik Rydgård 86fcfdc60a Add some new function names that were cracked from NIDs by zecoxao
He used Claude, never thought of this use case, but it's logical - it
can go reverse engineer the functions to come up with likely names,
making it easier to match the SHA1 hash. (NIDs are truncated SHA1 hashes
of names, except in firmwares later than around 3.50).
2026-09-10 15:52:53 -06:00
Henrik RydgårdandClaude Opus 5 e82980635b headless: don't fake a framebuffer descriptor for the end-of-run screenshot
SendDebugScreenshot ignores the descriptor entirely and reads the display
framebuffer from the GPU, so filling one in was theatre - and it computed a
pointer from the display address, which is zero whenever the shell has the
display switched off.

Also reset g_screenshotSaved per test, so a test that emits its own screenshot
doesn't stop the next one getting the end-of-run capture, and clear the sceReg
open count on shutdown to match init.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 15:30:03 -06:00
Henrik Rydgård 35d69dd4a1 De-claude a bit. It's so verbose. 2026-09-10 15:25:47 -06:00
Henrik RydgårdandClaude Opus 5 fda30ad99c HLE: add the 3.80/3.90 NIDs for the three VSH calls
Same three functions, one more NID each. Those two shells started no plugins at
all and sat on a black screen; both reach an interactive XMB now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 13:49:08 -06:00
Henrik RydgårdandClaude Opus 5 af5bc1db50 PrxDecrypter: add the 2.6x and 2.8x sceResmgr keys
2.60 and 2.71 tag their flash0:/vsh/etc/index.dat 0x495BE403 rather than with
the 0x0B2Bxxx0 the rest of this family uses; 2.80, 2.81 and 2.82 use 0x0B2B05F0.
All five now reach an interactive XMB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 13:49:08 -06:00
Henrik RydgårdandClaude Opus 5 f3f5432cb0 sceImpose: accept parameter 0x40000000
Every impose.prx from 1.50 to 5.55 dispatches on it and returns a field of the
impose context - the same field that decides where the backlight brightness
answer comes from. We rejected it as not a parameter at all, and 3.11's shell
read the error back, blanked the display with sceDisplaySetFrameBuf(0, 0, 0)
and never turned it on again.

2.00, 3.03 and 3.11 now reach an interactive XMB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 13:08:04 -06:00
Henrik RydgårdandClaude Opus 5 48a9498729 sceKernelModule: resolve the VSH's kernel drivers on old firmwares too
The per-model naming only starts around 3.50. Older dumps have plain
memlmd.prx and loadexec.prx, name the wlan firmware after the chip revision
(wlanfirm_magpie.prx became wlanfirm_01g.prx), and don't have lowio.prx at all
until about 3.52 - so every one of those was reported as a failed load on
2.00 through 3.30. Try the emulated model, the model in the path, then those
older spellings, and say plainly when the firmware simply doesn't ship one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:46:18 -06:00
Henrik RydgårdandClaude Opus 5 6b82202a2d PrxDecrypter: add the 3.0x, 3.1x and 3.5x sceResmgr keys too
Those generations predate the per-model split and have a single
flash0:/vsh/etc/index.dat. 3.30 through 3.52 now reach an interactive XMB;
3.30 to 3.51 share 3.52's key.

3.03's mesg_led.prx is older than keys330_1, so its table was confirmed against
keys300_1 and keys280_1 instead - both match byte for byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:46:18 -06:00
Henrik RydgårdandClaude Opus 5 f32c015659 PrxDecrypter: add the sceResmgr keys for the 5.0x, 6.0x and 6.3x XMB indexes
flash0:/vsh/etc/index_XXg.dat is the index of what the XMB shows, and each
firmware generation tags it with a key per PSP model. We only had the 6.6x
triple, so 5.03 through 6.39 decrypted nothing, drew no icons, and gave up with
the red error screen.

The keys come out of each firmware's own mesg_led_XXg.prx, whose tag table is
24-byte entries of tag plus 16-byte key. Extracting the 6.6x triple that way
reproduces the three keys already in this file byte for byte, which is what
establishes the layout.

5.03, 5.50, 5.55, 6.00, 6.20, 6.31 and 6.39 now reach an interactive XMB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:44:47 -06:00