Commit Graph
26 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 a361dfc490 Drop unused Config.h and SmallDataConvert.h includes
Config.h is edited about 100 times a year and each edit recompiled 198
files; now 172. SmallDataConvert.h pulls in arm_neon.h and was included by
headers that don't use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:13:42 +02:00
Henrik RydgårdandClaude Opus 5.5 023ad93ed3 sceVideocodec: Don't hold the ME for Init and Delete
They take tens of milliseconds for the caller, but queueing that time on
the shared ME timeline made the SAS mix wait behind them. In Jak and
Daxter that held up the sound threads at the end of the first clip, so
video_sound_thread got its last wake only after the game had deleted it
(NOT_DORMANT), and the orphaned thread then read a freed context.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:57:41 -06:00
Henrik RydgårdandClaude Opus 5.5 c2bc2d9308 ME: Charge measured times for sceVideocodec calls and the rest of sceAudiocodec
Measured on a PSP (pspautotests video/mp4/mp4timing, audio/audiocodec/timing):

- sceVideocodec Open, GetEDRAM, GetVersion and ReleaseEDRAM take ~70-150us,
  Init ~26.6ms (sceMpegCreate is 27-28ms), Delete ~21ms (was 2ms), and
  Stop 132us with nothing held back. All go through the ME queue now.
- Decodes that return no picture take as long as those that do; they
  were free.
- Open reports the EDRAM the decoder needs (0x3c2c) at ctx+0x18, which
  mpeg.prx passes on to GetEDRAM.
- sceAudiocodec: failed decodes (214/142/169us) and mono Atrac3+ init (524us).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:57:41 -06:00
Henrik RydgårdandClaude Opus 5.5 cd94e414cd Savestate: Reset what older states lack instead of keeping pre-load values
Missing sections (videocodec, audiocodec, aac, mp3) and old-version
branches (impose, io, umd, gps, mic, display, font) left the session
before the load in place.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 11:32:07 -06:00
Henrik RydgårdandClaude Opus 5.5 40a70b004c Savestate: Reset video frame tracking and ME busy time on load
Neither is serialized, and both went stale on load. The ME busy time was
measured against the pre-load clock, so loading an earlier state made the
next SAS/codec job wait until the old time came around, freezing the game.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 09:36:48 -06:00
Henrik RydgårdandClaude Opus 5.5 b39cf47124 Charge measured SAS mix time, and share the Media Engine between its jobs
The SAS mix estimate was a guess capped at 1200us. Measured on a PSP
(pspautotests audio/timing/sastiming), a mix costs 110us plus 0.49us per grain
sample, plus per voice and sample 0.445us + 0.0675us per unit of pitch ratio
(VAG; PCM and noise slightly less), plus 0.64us per sample with a reverb type
set. Linear to 1% across 64-2048 samples and 0-32 voices: 32 VAG voices at 512
samples take 8.7ms, not 1.2.

The mix runs on the Media Engine, as do video and audio decoding, so they now
queue behind each other there (MEScheduleJob). A game that keeps SAS running
during a movie - Star Wars: Lethal Alliance - decodes more slowly for it, like
on hardware.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 17:33:33 -06:00
Henrik RydgårdandClaude Opus 5.5 8f70003cce Remove the PaceVideocodecDecode workaround for Ys I & II
The blit cost model now paces its movies at 30 fps without it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 16:44:28 -06:00
Henrik RydgårdandClaude Opus 5.5 a0b812bc31 Charge hardware-measured time for movie decode, colour conversion and blit
Movie players like the one in Star Wars: Lethal Alliance present every decoded
frame after a single vblank wait, with no clock or timestamp check, so the
frame rate depends on decode, CSC, ATRAC decode and the GE blit adding up to
more than a vblank. We charged nearly nothing for any of them, so such movies
ran at 60 fps until the ringbuffer's slack ran out.

Costs measured on a PSP with a copy of that player (pspautotests
video/mpeg/playertiming), for a 480x272 frame:

- sceVideocodecDecode: 3.4ms (sceMpegAvcDecode 5.8ms less sceMpegAvcCsc 2.4ms)
- sceMpegBaseCscAvc: 2.4ms, was a flat 4ms
- sceAudiocodecDecode, ATRAC3+ only: 2.5ms per frame
- GE: 9.7ms for a through-mode rectangle blit from a decoded video frame,
  charged by area, only for textures in the buffer a decoder last wrote.

GE time also now carries across stall address updates. Before, a list sent
in stalled chunks only had its last chunk's time counted, so sceGeDrawSync
returned 39us after a blit that takes 9.7ms. This affects every game that
builds its lists incrementally, so GE-timing-sensitive games need checking.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 15:23:08 -06:00
Henrik RydgårdandClaude Opus 5 282647803c Add PaceVideocodecDecode compat.ini option
There are a few games that seem to rely on performance problems or
timing quirks to hold the correct pace when playing sceMpeg - have not
been able to nail down any other timing mechanism.

This adds a timing enforcement function.

Coded by Claude.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 09:00:48 -06:00
Henrik Rydgård 4d259a6f95 De-claude the comments. 2026-09-20 16:39:35 -06:00
Henrik RydgårdandClaude Opus 5 1a08f3c88c Give the Media Engine an address space instead of a table of packets
sceMpegBasePESpacketCopy used to gather each DMA into a buffer filed under the
address of its first block, and sceVideocodecDecode looked one up by the address
it was handed. That works only while an access unit arrives in a single call. It
doesn't: mpeg.prx routinely DMAs one in several pieces at consecutive addresses,
and the table then kept whichever piece happened to start where the decode later
asked, and dropped the rest.

So the DMA now writes each block at the address it names. The Media Engine's own
memory moves to its real addresses - low ones, which mpeg.prx bounds-checks
against 0x3FFFFF - and pieces written at consecutive addresses end up
consecutive, which is all this ever needed. Our own allocations move to the top
half, away from the addresses mpeg.prx picks for itself.

That memory is the ME's, not the Allegrex's, and the two are separate address
spaces: on hardware only the ME reaches it, so here it is a buffer of our own
that nothing emulated can address. It gets its own MEIsValidRange and
MEGetPointerRange rather than borrowing Memory::, which answers for the
Allegrex's memory and has nothing to say about this one.

An address only means something together with the space it came from, and
mpeg.prx hands over bare integers. The three places that have to work it out
from the value now ask the ME first, where they used to ask Memory:: first and
so read every address as the CPU's. It separates the addresses these callers
actually pass (ME memory well down in the first megabyte, PSP RAM at 0x08000000
and up), but that's those callers rather than a rule.

Savestates from before this carry the old packet table; it's read and dropped,
since the Media Engine's memory is saved and that's where the payloads live now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 16:39:35 -06:00
Henrik RydgårdandClaude Opus 5 5c5c7dec86 Include what these files use
UnitTest.h's EXPECT_ macros all call printf and EXPECT_EQ_MEM calls memcmp, but
it included neither <cstdio> nor <cstring> - it has been relying on whatever the
including file happened to pull in first, and TestMpegCsc was the first not to.
The same shape in sceMpegbase.cpp and sceVideocodec.cpp, which use std::min,
std::move and memcpy without saying where they come from.

Also drop an abs() from TestMpegCsc rather than include <cstdlib> for one
subtraction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:59:48 -06:00
Henrik RydgårdandClaude Opus 5 02906f0510 sceMpegbase: write alpha as zero, and stop rebuilding the planes every frame
The colour conversion was writing alpha fully set - 0xFF000000, or the top bit
for 5551 - where the hardware writes zero. Our sceMpeg HLE already masks it off
and names Sword Art Online as a game that depends on it: it doesn't clear the
alpha in the buffer it hands over, and expects the video not to set it. The two
paths now agree.

The de-tiling ahead of it becomes UntileYCbCr, taking the eight buffers already
resolved, so it can be measured and compared against the original longhand
version in TestMpegCsc. Its planes move to scratch that persists between calls -
a movie converts one frame per displayed frame, and this was allocating and
clearing about 200KB every time - and the per-pixel bounds checks in the chroma
loop, which only depend on the group of eight, are hoisted out of it.

That last part is worth 2529 -> 3201 MPix/s, but the point of measuring was to
find out whether it mattered, and it doesn't much: de-tiling is 0.04ms of a
frame against the conversion's 0.4ms. The conversion is where the time is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:38:49 -06:00
Henrik RydgårdandClaude Opus 5 b77e7c4e66 sceVideocodec: implement CopyYCbCr
This is what sceMpegAvcCopyYCbCr is built on, and a game that wants the raw
YCbCr rather than letting sceMpegbase convert to RGB uses it and nothing else.

mpeg.prx builds the descriptor on its own stack and avcodec.prx reads it back at
0x800015c4. Dimensions in pixels at 0x00/0x04, the eight frame buffers from 0x0c
but ordered 0,2,4,6 then 1,3,5,7, and from 0x2c the destination Y with Cb and Cr
following it contiguously - ordinary planar YUV420. Checked against what the
game passes: the eight addresses are exactly the buffers we handed out, and the
three destinations are spaced width*height and width*height/4 apart.

Un-tiling is the same operation the colour conversion already does, so that
moves out of sceMpegbase.cpp as ReadTiledYCbCr rather than being written twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 09:43:15 -06:00
Henrik RydgårdandClaude Opus 5 21928ed43f sceVideocodec: let stopping and deleting the decoder take time
Both are ME round-trips that take real time on hardware, and returning from them
immediately matters beyond speed. Jak and Daxter deletes its video_sound_thread
straight after sceVideocodecDelete without waiting for it to exit. With no time
passing in the delete, the game's audio thread never gets to run once more and
deliver the wake that lets that thread notice the shutdown and exit, so
sceKernelDeleteThread fails with NOT_DORMANT and the thread stays alive. It is
then released from the event flag the game has just deleted, resumes on a
context that has already been freed - every id and pointer in it zero - and
copies from a null pointer.

2ms, chosen to sit above the 1.45ms an audio mix block takes. 100us was measured
to be too short, so the fix is the time passing rather than just the reschedule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:18:06 -06:00
Henrik RydgårdandClaude Opus 5 0207c6d04a Clear the codec context maps per boot, and replace functions in headless too
sceMpeg and sceAudiocodec only cleared their context maps on shutdown, while
sceMpegbase and sceVideocodec clear theirs on init. Both maps are keyed on an
address the game chooses, and getMpegCtx reads its key straight out of game
memory, so anything left behind can be handed to the next game we run in the
same session. Clear them on init as well. sceVideocodec's init cleared its map
without deleting the decoders in it; use ClearContexts for that.

Headless never loads a config file, so every g_Config field it doesn't set
keeps the zero-initialized value rather than the ConfigSetting default.
bFuncReplacements is one of those, so headless was the only build running games
without function replacements - which is why a crash in Jak and Daxter's
memcpy_jak wouldn't reproduce there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 12:51:52 -06:00
Henrik RydgårdandClaude Opus 5 c596bdf9a4 sceAudiocodec/sceVideocodec: fill an AAC gap and name two ME functions
sceAudiocodecCheckNeedMem set neededMem for every codec except AAC, where it
was left at whatever was in the context. Set it to 0x18f20 like the others; our
faked ME memory meant this never blocked, but a game that validates it could.

Also name the two hash-named sceVideocodec exports from their firmware
behaviour: 0x893B32B1 configures the codec during sceMpegCreate in mode 1
(SetMode), and 0xD95C24D5 copies a decoded YCbCr frame between buffers via the
ME (CopyYCbCr), the videocodec-level counterpart of sceMpegBaseYCrCbCopy. Both
are still stubs - only mpeg.prx calls them, on paths nothing we run reaches -
but they are now documented for whoever implements them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:01:09 -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 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ård 10a9d7bb31 De-claude some overly verbose comments 2026-09-14 10:42:55 -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 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 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 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