Files
ppsspp/Core/HLE/sceMpegbase.cpp
T
Henrik RydgårdandClaude Opus 5 dc743d4fce sceMpegbase: free the conversion's memory on shutdown
__MpegBaseShutdown, hanging off __MpegShutdown the way __MpegBaseInit hangs off
__MpegInit, so the de-tiling scratch and the swscale context go back when the
game stops rather than only when the next one starts. Between them they are a
few hundred kilobytes that a game which played one video early on has no further
use for.

Also name the swscale flags rather than passing SWS_POINT inline, and say next
to it what the choice actually decides - with equal sizes in and out it is only
how chroma gets to full resolution, and SWS_BILINEAR (what the HLE uses) is a
one-line swap. Worth a real option one day; not adding one now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:38:49 -06:00

641 lines
26 KiB
C++

// Copyright (c) 2026- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
// sceMpegbase - the Media Engine's colour space conversion, and the DMA that feeds it.
//
// mpeg.prx drives these directly, so they have to be real for the firmware module to run in place
// of our sceMpeg HLE.
#include <map>
#include <vector>
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Common/Serialize/SerializeMap.h"
#include "Common/Swap.h"
#include "Core/HLE/HLE.h"
#include "Core/HLE/FunctionWrappers.h"
#include "Core/HLE/sceMpeg.h"
#include "Core/HLE/sceMpegbase.h"
#include "Core/HLE/sceVideocodec.h"
#include "Core/MemMapHelpers.h"
#include "GPU/GPUCommon.h"
#include "GPU/GPUState.h"
#include "GPU/ge_constants.h"
#ifdef USE_FFMPEG
extern "C" {
#include "libswscale/swscale.h"
#include "libavutil/pixfmt.h"
}
#endif
// The PES payloads gathered by sceMpegBasePESpacketCopy, keyed by the destination each was
// copied to. It carries audio as well as video - the destination is what tells them apart - so
// sceVideocodec has to ask for the one matching the address it was handed.
static std::map<u32, std::vector<u8>> g_pesPackets;
// Set by sceMpegBaseCscInit / sceMpegBaseCscSetPixelMode, and used when the caller passes 0.
static int g_mpegBaseBufferWidth = 512;
static int g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888;
// Scratch for the planes the de-tiling produces. Reused between calls: a movie converts one of
// these every frame, and they are a couple of hundred kilobytes, so allocating them per call was
// pure overhead. Nothing here needs saving - it is rebuilt from the ME's buffers on every call.
static std::vector<u8> g_untileScratch;
void __MpegBaseInit() {
// None of this survives a boot on hardware.
g_pesPackets.clear();
g_untileScratch.clear();
g_untileScratch.shrink_to_fit();
MpegCscShutdown();
g_mpegBaseBufferWidth = 512;
g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888;
}
void __MpegBaseShutdown() {
// The scratch and the swscale context are worth a few hundred kilobytes between them, and a
// game that played one video early on has no use for either afterwards.
g_pesPackets.clear();
g_untileScratch.clear();
g_untileScratch.shrink_to_fit();
MpegCscShutdown();
}
void __MpegBaseDoState(PointerWrap &p) {
auto s = p.Section("sceMpegbase", 0, 1);
if (!s) {
return;
}
// The pixel mode decides both the colour packing and the bytes per pixel of the output, and a
// game sets it once per movie rather than per frame - so without it here, a state resumed
// mid-movie converted at the default until the next sceMpegBaseCscInit, which may never come.
Do(p, g_mpegBaseBufferWidth);
Do(p, g_mpegBasePixelMode);
// A state can land between the copy and the decode that consumes it.
Do(p, g_pesPackets);
}
// p pointing to a SceMpegLLI structure consists of video frame blocks.
static u32 sceMpegBasePESpacketCopy(u32 p)
{
int nBlocks = 0;
auto lli = PSPPointer<SceMpegLLI>::Create(p);
while (lli.IsValid()) {
nBlocks++;
if (lli->Next == 0) {
// Last block
break;
}
++lli;
}
MpegSetPmpVideoSource(p, nBlocks);
// On hardware this is the DMA that moves the PES payload into the Media Engine's own memory,
// after which mpeg.prx hands sceVideocodecDecode an ME-side address we have no way to read.
// Since the copy is ours, gather the blocks here instead and let sceVideocodec decode from
// this - see MpegBaseTakePESPacket.
lli = PSPPointer<SceMpegLLI>::Create(p);
u32 dest = 0;
std::vector<u8> gathered;
for (int i = 0; i < nBlocks && lli.IsValid(); i++) {
if (i == 0) {
dest = lli->pDst;
}
// The list is game-supplied, so check the span validity before taking a pointer to it.
const u8 *src = (lli->iSize > 0 && Memory::IsValidRange(lli->pSrc, lli->iSize))
? Memory::GetTypedPointerRange<u8>(lli->pSrc, lli->iSize) : nullptr;
if (src) {
gathered.insert(gathered.end(), src, src + lli->iSize);
// Audio payloads land in main memory, and mpeg.prx hands that same address to
// sceAudiocodecDecode as its input, so for those the copy has to really happen.
// Video goes to a Media Engine address that isn't mapped for us: the gather above
// is what stands in for it there.
if (Memory::IsValidRange(lli->pDst, lli->iSize)) {
Memory::MemcpyUnchecked(lli->pDst, src, lli->iSize);
}
}
++lli;
}
if (dest != 0) {
g_pesPackets[dest] = std::move(gathered);
}
DEBUG_LOG(Log::Mpeg, "sceMpegBasePESpacketCopy(%08x), %d block(s) -> %08x, %d bytes",
p, nBlocks, dest, dest ? (int)g_pesPackets[dest].size() : 0);
return 0;
}
std::vector<u8> MpegBaseTakePESPacket(u32 dest) {
auto it = g_pesPackets.find(dest);
if (it == g_pesPackets.end()) {
return std::vector<u8>();
}
std::vector<u8> packet = std::move(it->second);
g_pesPackets.erase(it);
return packet;
}
// --- sceMpegbase colour conversion ---------------------------------------------------------
//
// mpeg.prx hands the ME's decoded output to these to be converted to RGB. The descriptor it
// passes is 48 bytes, which matches the range mpegbase.prx bounds-checks before using it.
//
// mpeg.prx builds this on its own stack before each call (1.3 at 08805698, 1.8 at 08805898, the
// same shape) from the eight buffer addresses sceVideocodec published and the dimensions from its
// own context. mpegbase.prx reads the dimensions at 0x00/0x04 and the eight buffers at 0x10..0x2c.
//
// The buffers live either in Media Engine memory (a freshly decoded frame) or in the game's
// memory (once sceMpegBaseYCrCbCopy has moved one out).
struct SceMp4AvcCscStruct {
s32_le height; // 0x00 in macroblocks
s32_le width; // 0x04
s32_le unk08; // 0x08
s32_le unk0c; // 0x0c
u32_le buffer[8]; // 0x10 four luma, then four chroma
};
static_assert(sizeof(SceMp4AvcCscStruct) == 0x30);
// A frame buffer is either in the Media Engine's memory or, after a copy, in the game's.
static const u8 *MpegBaseFramePointer(u32 addr, int size) {
if (const u8 *me = VideocodecMEPointer(addr, size)) {
return me;
}
return Memory::GetTypedPointerRange<u8>(addr, size);
}
// The ME doesn't write plain planar YCbCr. The layout below was established by analysing
// sceMpegBaseYCrCbCopy output on a real PSP (documented in JPCSP's sceVideocodec), and it uses
// all eight buffers in the descriptor:
//
// The image is divided into vertical bands 32 pixels wide, each split into two 16-pixel halves.
// Luma is one byte per pixel, and which buffer a row lands in depends on whether it is even or
// odd:
// buffer0: left half, even rows buffer1: right half, even rows
// buffer2: left half, odd rows buffer3: right half, odd rows
// Within a buffer, rows are stored 16 bytes at a time, band by band, top to bottom.
//
// Chroma is one (Cb,Cr) byte pair per 2x2 pixel square, so in chroma coordinates the bands are
// 16 wide with 8-pixel halves, and the same even/odd split applies:
// buffer4: left half, even chroma rows buffer5: right half, even chroma rows
// buffer6: left half, odd chroma rows buffer7: right half, odd chroma rows
//
// Untangling it into plain planes costs one pass per frame, which keeps the conversion below
// readable and is not where the time goes.
// The de-tiling itself, with the address resolution left outside so it can be measured and
// checked on its own - see TestMpegCsc. src is the eight buffers in sceVideocodec order (four
// luma, then four chroma) and sizes says how big each one is.
//
// Every byte of the output is written for any frame the hardware can produce, so the caller does
// not have to clear it first.
void UntileYCbCr(u8 *luma, u8 *cb, u8 *cr, const u8 *const src[8], const int sizes[8],
int width, int height) {
const int width2 = width >> 1;
const int height2 = height >> 1;
const int *ySize = sizes;
const int *cSize = sizes + 4;
// Luma: four buffers, keyed by (left/right half of the band, even/odd row).
for (int b = 0; b < 4; b++) {
if (!src[b]) {
continue;
}
const int xOffset = (b & 1) ? 16 : 0;
const int yStart = (b >> 1) ? 1 : 0;
int j = 0;
for (int bandX = xOffset; bandX < width; bandX += 32) {
const int run = std::min(16, width - bandX);
for (int row = yStart; row < height; row += 2, j += 16) {
if (run <= 0 || j + run > ySize[b]) {
continue;
}
memcpy(luma + (size_t)row * width + bandX, src[b] + j, run);
}
}
}
// Chroma: same shape in half-resolution coordinates, but the two planes arrive interleaved as
// (Cb,Cr) pairs, so each group of 8 pixels is a 16-byte run to pull apart. The bounds the old
// version checked per pixel only depend on the group, so they are hoisted out here - that inner
// loop was the expensive half of this function.
for (int b = 0; b < 4; b++) {
if (!src[b + 4]) {
continue;
}
const int xOffset = (b & 1) ? 8 : 0;
const int yStart = (b >> 1) ? 1 : 0;
int j = 0;
for (int bandX = xOffset; bandX < width2; bandX += 16) {
for (int row = yStart; row < height2; row += 2, j += 16) {
// How many of the 8 fit both in the row and in what the buffer actually holds.
const int fits = std::min(8, (cSize[b] - j) >> 1);
const int run = std::min(width2 - bandX, fits);
if (run <= 0) {
continue;
}
const u8 *from = src[b + 4] + j;
u8 *toCb = cb + (size_t)row * width2 + bandX;
u8 *toCr = cr + (size_t)row * width2 + bandX;
for (int k = 0; k < run; k++) {
toCb[k] = from[k * 2];
toCr[k] = from[k * 2 + 1];
}
}
}
}
}
bool ReadTiledYCbCr(const u32 *buffers, int width, int height,
const u8 **luma, const u8 **cb, const u8 **cr) {
int sizes[8];
VideocodecFrameBufferLayout(width, height, sizes, nullptr);
const u8 *src[8]{};
for (int i = 0; i < 8; i++) {
if (sizes[i] > 0) {
src[i] = MpegBaseFramePointer(buffers[i], sizes[i]);
if (!src[i]) {
return false;
}
}
}
const size_t lumaBytes = (size_t)width * height;
const size_t chromaBytes = (size_t)(width >> 1) * (height >> 1);
if (g_untileScratch.size() < lumaBytes + chromaBytes * 2) {
g_untileScratch.resize(lumaBytes + chromaBytes * 2);
}
u8 *l = g_untileScratch.data();
u8 *b = l + lumaBytes;
u8 *r = b + chromaBytes;
UntileYCbCr(l, b, r, src, sizes, width, height);
*luma = l;
*cb = b;
*cr = r;
return true;
}
static u32 YCbCrToPixel(int y, int cbv, int crv, int pixelMode) {
const int c = y - 16, d = cbv - 128, e = crv - 128;
int r = (298 * c + 409 * e + 128) >> 8;
int g = (298 * c - 100 * d - 208 * e + 128) >> 8;
int b = (298 * c + 516 * d + 128) >> 8;
r = std::min(255, std::max(0, r));
g = std::min(255, std::max(0, g));
b = std::min(255, std::max(0, b));
// Alpha comes out zero, not opaque. That is what the hardware does - our sceMpeg HLE masks it
// off for the same reason, and names Sword Art Online as a game that depends on it, because it
// doesn't clear the alpha in the buffer it hands over and expects the video to leave it clear.
switch (pixelMode) {
case GE_CMODE_16BIT_BGR5650:
return ((b >> 3) << 11) | ((g >> 2) << 5) | (r >> 3);
case GE_CMODE_16BIT_ABGR5551:
return ((b >> 3) << 10) | ((g >> 3) << 5) | (r >> 3);
case GE_CMODE_16BIT_ABGR4444:
return ((b >> 4) << 8) | ((g >> 4) << 4) | (r >> 4);
default:
return (b << 16) | (g << 8) | r;
}
}
// The conversion itself, with nothing around it. Pure, so that TestMpegCsc can measure it and
// check it - it is the hottest thing in video playback, and the point of having it out here is
// that it can be worked on without a game in the loop.
//
// luma is width by height; cb and cr are half that in both directions, as YUV420 is. dest is
// destStride pixels wide in the format pixelMode names, and the converted range always lands at
// its origin.
void MpegCscRangeScalar(u8 *dest, int destStride, int pixelMode,
const u8 *luma, const u8 *cb, const u8 *cr, int width,
int rangeX, int rangeY, int rangeWidth, int rangeHeight) {
const int bpp = pixelMode == GE_CMODE_32BIT_ABGR8888 ? 4 : 2;
const int width2 = width >> 1;
for (int y = 0; y < rangeHeight; y++) {
const int sy = rangeY + y;
for (int x = 0; x < rangeWidth; x++) {
const int sx = rangeX + x;
const int ci = (sy >> 1) * width2 + (sx >> 1);
const u32 pixel = YCbCrToPixel(luma[sy * width + sx], cb[ci], cr[ci], pixelMode);
if (bpp == 4) {
memcpy(dest + (y * destStride + x) * 4, &pixel, 4);
} else {
const u16 p16 = (u16)pixel;
memcpy(dest + (y * destStride + x) * 2, &p16, 2);
}
}
}
}
#ifdef USE_FFMPEG
// swscale is what our sceMpeg HLE converts with, and the planes the de-tiling produces are
// already the YUV420P it wants, so the same thing works here - and it is a great deal quicker
// than doing it a pixel at a time.
//
// The four output formats are the ones MediaEngine::getSwsFormat picks, for the same reasons.
// Alpha is not among them: swscale writes RGBA opaque and leaves the spare bits of the 16-bit
// formats clear, so the masking below is what makes the result match the hardware, exactly as
// the HLE does after its own sws_scale.
static AVPixelFormat SwsFormatForPixelMode(int pixelMode) {
switch (pixelMode) {
case GE_CMODE_16BIT_BGR5650: return AV_PIX_FMT_BGR565LE;
case GE_CMODE_16BIT_ABGR5551: return AV_PIX_FMT_BGR555LE;
case GE_CMODE_16BIT_ABGR4444: return AV_PIX_FMT_BGR444LE;
default: return AV_PIX_FMT_RGBA;
}
}
// Nothing is being scaled here, so this only picks how chroma reaches full resolution: SWS_POINT
// repeats each 2x2 block's sample, as the scalar path and presumably the hardware do, while
// SWS_BILINEAR smooths between samples, as our sceMpeg HLE does. Swap the line to taste - it
// deserves a real option eventually.
static const int MPEG_CSC_SWS_FLAGS = SWS_POINT;
static SwsContext *g_cscSws;
static int g_cscSwsWidth, g_cscSwsHeight, g_cscSwsFormat = -1;
void MpegCscShutdown() {
if (g_cscSws) {
sws_freeContext(g_cscSws);
g_cscSws = nullptr;
}
g_cscSwsWidth = 0;
g_cscSwsHeight = 0;
g_cscSwsFormat = -1;
}
bool MpegCscRangeSws(u8 *dest, int destStride, int pixelMode,
const u8 *luma, const u8 *cb, const u8 *cr, int width,
int rangeX, int rangeY, int rangeWidth, int rangeHeight) {
// Chroma is half resolution, so an odd origin would start half a sample in and there is no way
// to say that to swscale. Nothing can actually ask for one - the ranges arrive in macroblocks -
// but the scalar path is still there for it.
if ((rangeX & 1) || (rangeY & 1)) {
return false;
}
const AVPixelFormat format = SwsFormatForPixelMode(pixelMode);
if (rangeWidth != g_cscSwsWidth || rangeHeight != g_cscSwsHeight || (int)format != g_cscSwsFormat) {
g_cscSws = sws_getCachedContext(g_cscSws, rangeWidth, rangeHeight, AV_PIX_FMT_YUV420P,
rangeWidth, rangeHeight, format, MPEG_CSC_SWS_FLAGS, nullptr, nullptr, nullptr);
if (!g_cscSws) {
return false;
}
// Studio swing both ways, which is the range the coefficients in the scalar path assume.
int *invCoeff, *coeff, srcRange, dstRange, brightness, contrast, saturation;
if (sws_getColorspaceDetails(g_cscSws, &invCoeff, &srcRange, &coeff, &dstRange, &brightness,
&contrast, &saturation) != -1) {
sws_setColorspaceDetails(g_cscSws, invCoeff, 0, coeff, 0, brightness, contrast, saturation);
}
g_cscSwsWidth = rangeWidth;
g_cscSwsHeight = rangeHeight;
g_cscSwsFormat = (int)format;
}
const int width2 = width >> 1;
const u8 *srcSlice[4] = {
luma + (size_t)rangeY * width + rangeX,
cb + (size_t)(rangeY >> 1) * width2 + (rangeX >> 1),
cr + (size_t)(rangeY >> 1) * width2 + (rangeX >> 1),
nullptr,
};
const int srcStride[4] = { width, width2, width2, 0 };
const int bpp = pixelMode == GE_CMODE_32BIT_ABGR8888 ? 4 : 2;
u8 *dstSlice[4] = { dest, nullptr, nullptr, nullptr };
const int dstStride[4] = { destStride * bpp, 0, 0, 0 };
if (sws_scale(g_cscSws, srcSlice, srcStride, 0, rangeHeight, dstSlice, dstStride) <= 0) {
return false;
}
// Clear the alpha swscale filled in, which the hardware leaves at zero.
for (int y = 0; y < rangeHeight; y++) {
u8 *row = dest + (size_t)y * destStride * bpp;
if (bpp == 4) {
u32_le *p32 = (u32_le *)row;
for (int x = 0; x < rangeWidth; x++) {
p32[x] = p32[x] & 0x00FFFFFF;
}
} else if (pixelMode != GE_CMODE_16BIT_BGR5650) {
const u16 mask = pixelMode == GE_CMODE_16BIT_ABGR5551 ? 0x7FFF : 0x0FFF;
u16_le *p16 = (u16_le *)row;
for (int x = 0; x < rangeWidth; x++) {
p16[x] = p16[x] & mask;
}
}
}
return true;
}
#else // !USE_FFMPEG
bool MpegCscRangeSws(u8 *dest, int destStride, int pixelMode,
const u8 *luma, const u8 *cb, const u8 *cr, int width,
int rangeX, int rangeY, int rangeWidth, int rangeHeight) {
return false;
}
void MpegCscShutdown() {}
#endif // USE_FFMPEG
void MpegCscRange(u8 *dest, int destStride, int pixelMode,
const u8 *luma, const u8 *cb, const u8 *cr, int width,
int rangeX, int rangeY, int rangeWidth, int rangeHeight) {
#ifdef USE_FFMPEG
if (MpegCscRangeSws(dest, destStride, pixelMode, luma, cb, cr, width,
rangeX, rangeY, rangeWidth, rangeHeight)) {
return;
}
#endif
MpegCscRangeScalar(dest, destStride, pixelMode, luma, cb, cr, width,
rangeX, rangeY, rangeWidth, rangeHeight);
}
// The shared body of sceMpegBaseCscAvc and sceMpegBaseCscAvcRange - the former is just the
// latter over the whole frame.
static int MpegBaseCscRange(u32 bufferRGB, u32 cscAddr, int bufferWidth,
int rangeX, int rangeY, int rangeWidth, int rangeHeight) {
auto csc = PSPPointer<SceMp4AvcCscStruct>::Create(cscAddr);
if (!csc.IsValid()) {
return hleLogError(Log::Mpeg, -1, "bad csc struct pointer");
}
if (bufferWidth == 0) {
bufferWidth = g_mpegBaseBufferWidth;
}
u32 buffers[8]{};
for (int i = 0; i < 8; i++) {
buffers[i] = csc->buffer[i];
}
const int width = csc->width << 4;
const int height = csc->height << 4;
if (width <= 0 || height <= 0 || width > 1024 || height > 1024) {
return hleLogError(Log::Mpeg, -1, "unreasonable frame size %dx%d", width, height);
}
if (rangeWidth <= 0 || rangeHeight <= 0) {
return hleLogDebug(Log::Mpeg, 0, "empty range");
}
rangeWidth = std::min(rangeWidth, width - rangeX);
rangeHeight = std::min(rangeHeight, height - rangeY);
// The output buffer is sized from the stride below, so a row can't be wider than one. Every
// game seen so far passes a stride comfortably wider than the frame (512 for 480), but nothing
// guarantees it, and writing a wider row than we measured would run off the end of the buffer.
rangeWidth = std::min(rangeWidth, bufferWidth);
if (rangeX < 0 || rangeY < 0 || rangeWidth <= 0 || rangeHeight <= 0) {
return hleLogError(Log::Mpeg, -1, "range outside the frame");
}
const u8 *luma, *cb, *cr;
if (!ReadTiledYCbCr(buffers, width, height, &luma, &cb, &cr)) {
return hleLogError(Log::Mpeg, -1, "YCbCr buffers not readable");
}
const int bpp = g_mpegBasePixelMode == GE_CMODE_32BIT_ABGR8888 ? 4 : 2;
const u32 destSize = (u32)(rangeHeight * bufferWidth * bpp);
if (!Memory::IsValidRange(bufferRGB, destSize)) {
return hleLogError(Log::Mpeg, -1, "output buffer not writable");
}
u8 *dest = Memory::GetTypedPointerWriteRange<u8>(bufferRGB, destSize);
if (!dest) {
return hleLogError(Log::Mpeg, -1, "output buffer not writable");
}
MpegCscRange(dest, bufferWidth, g_mpegBasePixelMode, luma, cb, cr, width,
rangeX, rangeY, rangeWidth, rangeHeight);
NotifyMemInfo(MemBlockFlags::WRITE, bufferRGB, destSize, "MpegBaseCsc");
// The CPU just wrote a video frame into what is usually a display buffer. The hardware backends
// don't see that on their own, so without telling them the screen keeps showing the last frame
// the GE drew (the same notification our sceMpegAvcCsc HLE does). The pixel mode numbering
// matches GEBufferFormat, as it does there.
gpu->PerformWriteFormattedFromMemory(bufferRGB, destSize, bufferWidth, (GEBufferFormat)g_mpegBasePixelMode);
// This runs on the DMACPLUS and takes real time. A psmfplayer game blits the current video
// frame every render frame while it waits for the next, so an instant return is a tight loop
// that never yields and starves the audio thread that paces playback, and the A/V pipeline
// deadlocks a few frames in (SOCOM: Tactical Strike hangs exactly here). Our sceMpeg HLE delays
// sceMpegAvcCsc the same way.
return hleDelayResult(hleLogDebug(Log::Mpeg, 0, "%dx%d at %d,%d -> %08x stride %d",
rangeWidth, rangeHeight, rangeX, rangeY, bufferRGB, bufferWidth), "mpegbase csc", 4000);
}
static int sceMpegBaseCscInit(int bufferWidth) {
g_mpegBaseBufferWidth = bufferWidth ? bufferWidth : 512;
return hleLogInfo(Log::Mpeg, 0, "bufferWidth %d", bufferWidth);
}
// Sets the output pixel format for the conversions below. The official name isn't known - this one
// says what it does. Inside mpegbase.prx it stores the value and hands it to the DMACPLUS colour
// conversion hardware, and mpeg.prx gets it by indexing a table of {1, 2, 3, 0} with the pixel mode
// the game passed to sceMpegAvcDecodeMode. So the numbering is its own, not the GE's.
static int sceMpegBaseCscSetPixelMode(int pixelMode) {
static const int toGeMode[4] = {
GE_CMODE_32BIT_ABGR8888,
GE_CMODE_16BIT_BGR5650,
GE_CMODE_16BIT_ABGR5551,
GE_CMODE_16BIT_ABGR4444,
};
if (pixelMode < 0 || pixelMode >= (int)ARRAY_SIZE(toGeMode)) {
return hleLogError(Log::Mpeg, -1, "bad pixel mode %d", pixelMode);
}
g_mpegBasePixelMode = toGeMode[pixelMode];
return hleLogInfo(Log::Mpeg, 0, "pixel mode %d -> GE mode %d", pixelMode, g_mpegBasePixelMode);
}
static int sceMpegBaseCscAvc(u32 bufferRGB, u32 unknown, int bufferWidth, u32 cscAddr) {
auto csc = PSPPointer<SceMp4AvcCscStruct>::Create(cscAddr);
if (!csc.IsValid()) {
return hleLogError(Log::Mpeg, -1, "bad csc struct pointer");
}
// The whole frame. MpegBaseCscRange clamps to the real frame size (from the allocation, not the
// descriptor), so pass more than any frame can be.
return MpegBaseCscRange(bufferRGB, cscAddr, bufferWidth, 0, 0, 1024, 1024);
}
static u32 sceMpegBaseCscAvcRange(u32 bufferRGB, u32 unknown, u32 rangeAddr, int bufferWidth, u32 cscAddr) {
if (!Memory::IsValidRange(rangeAddr, 16)) {
return hleLogError(Log::Mpeg, -1, "bad range pointer");
}
// Also in macroblocks.
const int rangeX = Memory::ReadUnchecked_U32(rangeAddr + 0) << 4;
const int rangeY = Memory::ReadUnchecked_U32(rangeAddr + 4) << 4;
const int rangeWidth = Memory::ReadUnchecked_U32(rangeAddr + 8) << 4;
const int rangeHeight = Memory::ReadUnchecked_U32(rangeAddr + 12) << 4;
return MpegBaseCscRange(bufferRGB, cscAddr, bufferWidth, rangeX, rangeY, rangeWidth, rangeHeight);
}
// Moves a decoded frame between two sets of buffers. Both arguments are descriptors; what gets
// copied is the pixels they point at, not the descriptors.
//
// mpegbase.prx builds a DMA list over the eight buffers (080010f8 in mpegbase_260.prx): flag bit 0
// selects buffers 0,1,4,5 and bit 1 selects 2,3,6,7, with the per-buffer sizes sceVideocodec lays
// its frame out with. mpeg.prx always passes 3 (all eight). The source is the ME's frame; the
// destination is wherever the caller wants it (for psmfplayer, a slot in its output pool).
static int sceMpegBaseYCrCbCopy(u32 dstAddr, u32 srcAddr, int flags) {
auto dst = PSPPointer<SceMp4AvcCscStruct>::Create(dstAddr);
auto src = PSPPointer<SceMp4AvcCscStruct>::Create(srcAddr);
if (!dst.IsValid() || !src.IsValid()) {
return hleLogError(Log::Mpeg, -1, "bad descriptor pointer");
}
// The destination descriptor carries the dimensions in macroblocks (as the colour conversion's
// does); the stack-built source descriptor states them in pixels, so read the destination.
const int width = dst->width << 4;
const int height = dst->height << 4;
if (width <= 0 || height <= 0 || width > 1024 || height > 1024) {
return hleLogError(Log::Mpeg, -1, "unreasonable frame size %dx%d", width, height);
}
int sizes[8];
VideocodecFrameBufferLayout(width, height, sizes, nullptr);
int copied = 0;
for (int i = 0; i < 8; i++) {
// Buffers 0,1,4,5 go with bit 0 and 2,3,6,7 with bit 1 (the even and odd row halves of luma
// and chroma).
const int bit = ((i & 3) < 2) ? 1 : 2;
if (!(flags & bit) || sizes[i] <= 0) {
continue;
}
const u8 *from = MpegBaseFramePointer(src->buffer[i], sizes[i]);
if (!from) {
return hleLogError(Log::Mpeg, -1, "source buffer %d not readable", i);
}
if (!Memory::IsValidRange(dst->buffer[i], sizes[i])) {
return hleLogError(Log::Mpeg, -1, "destination buffer %d (%08x, %d bytes) not writable",
i, (u32)dst->buffer[i], sizes[i]);
}
Memory::MemcpyUnchecked(dst->buffer[i], from, sizes[i]);
copied += sizes[i];
}
return hleLogDebug(Log::Mpeg, 0, "flags %d, %dx%d, %d bytes", flags, width, height, copied);
}
const HLEFunction sceMpegbase[] =
{
{0XBEA18F91, &WrapU_U<sceMpegBasePESpacketCopy>, "sceMpegBasePESpacketCopy", 'x', "x" },
{0X492B5E4B, &WrapI_I<sceMpegBaseCscInit>, "sceMpegBaseCscInit", 'i', "i" },
{0X0530BE4E, &WrapI_I<sceMpegBaseCscSetPixelMode>, "sceMpegbase_0530BE4E", 'i', "i" },
{0X91929A21, &WrapI_UUIU<sceMpegBaseCscAvc>, "sceMpegBaseCscAvc", 'i', "xxix" },
{0X304882E1, &WrapU_UUUIU<sceMpegBaseCscAvcRange>, "sceMpegBaseCscAvcRange", 'x', "xxxix" },
{0X7AC0321A, &WrapI_UUI<sceMpegBaseYCrCbCopy>, "sceMpegBaseYCrCbCopy", 'i', "xxi" }
};
void Register_sceMpegbase()
{
RegisterHLEModule("sceMpegbase", ARRAY_SIZE(sceMpegbase), sceMpegbase);
};