Files
ppsspp/Core/HLE/sceMpegbase.cpp
T

409 lines
16 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/ge_constants.h"
// 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;
void __MpegBaseInit() {
// None of this survives a boot on hardware.
g_pesPackets.clear();
g_mpegBaseBufferWidth = 512;
g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888;
}
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.
//
// The dimensions appear twice. Death Jr. has the plain macroblock counts in both pairs, but
// Thrillville's psmfplayer path has them shifted up by 8 in the first pair and plain in the
// second, so the second is the one to trust. The four luma buffers are at 0x20; the chroma ones
// aren't in here at all (see the recovery in MpegBaseCscRange).
struct SceMp4AvcCscStruct {
s32_le scaledHeight; // 0x00 height << 8 on some paths, macroblocks on others - unused
s32_le scaledWidth; // 0x04
s32_le mode0; // 0x08
s32_le mode1; // 0x0c
s32_le height; // 0x10 in macroblocks
s32_le width; // 0x14 in macroblocks
s32_le unk18; // 0x18
s32_le unk1c; // 0x1c
u32_le buffer[4]; // 0x20 luma only
};
static_assert(sizeof(SceMp4AvcCscStruct) == 0x30);
// 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: left half, odd chroma rows
// buffer6: right half, even 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.
static bool ReadTiledYCbCr(const u32 *buffers, int width, int height,
std::vector<u8> &luma, std::vector<u8> &cb, std::vector<u8> &cr) {
const int width2 = width >> 1;
const int height2 = height >> 1;
int sizes[8];
VideocodecFrameBufferLayout(width, height, sizes, nullptr);
const int *ySize = sizes;
const int *cSize = sizes + 4;
// These are addresses in the Media Engine's memory, not in PSP RAM, so they resolve through
// sceVideocodec rather than through Memory::. A descriptor that was never filled in holds
// small integers instead, and those simply aren't in the ME's range.
const u8 *y[4] = {};
const u8 *c[4] = {};
for (int i = 0; i < 4; i++) {
if (ySize[i] > 0) {
y[i] = VideocodecMEPointer(buffers[i], ySize[i]);
if (!y[i]) {
return false;
}
}
if (cSize[i] > 0) {
c[i] = VideocodecMEPointer(buffers[4 + i], cSize[i]);
if (!c[i]) {
return false;
}
}
}
luma.assign((size_t)width * height, 0);
cb.assign((size_t)width2 * height2, 128);
cr.assign((size_t)width2 * height2, 128);
// Luma: four buffers, keyed by (left/right half of the band, even/odd row).
for (int b = 0; b < 4; b++) {
if (!y[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], y[b] + j, run);
}
}
}
// Chroma: same shape in half-resolution coordinates, with interleaved Cb/Cr pairs.
for (int b = 0; b < 4; b++) {
if (!c[b]) {
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) {
for (int k = 0; k < 8; k++, j += 2) {
const int x = bandX + k;
if (x >= width2 || j + 1 >= cSize[b]) {
continue;
}
const size_t i = (size_t)row * width2 + x;
cb[i] = c[b][j];
cr[i] = c[b][j + 1];
}
}
}
}
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));
switch (pixelMode) {
case GE_CMODE_16BIT_BGR5650:
return ((b >> 3) << 11) | ((g >> 2) << 5) | (r >> 3);
case GE_CMODE_16BIT_ABGR5551:
return (1 << 15) | ((b >> 3) << 10) | ((g >> 3) << 5) | (r >> 3);
case GE_CMODE_16BIT_ABGR4444:
return (0xF << 12) | ((b >> 4) << 8) | ((g >> 4) << 4) | (r >> 4);
default:
return 0xFF000000 | (b << 16) | (g << 8) | r;
}
}
// 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;
}
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");
}
// The descriptor only carries the four luma buffers. Recover the full set from the allocation
// sceVideocodec handed out - both ends of that are ours.
u32 buffers[8]{};
for (int i = 0; i < 4; i++) {
buffers[i] = csc->buffer[i];
}
VideocodecGetFrameBuffers(csc->buffer[0], buffers);
std::vector<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");
}
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], g_mpegBasePixelMode);
if (bpp == 4) {
memcpy(dest + (y * bufferWidth + x) * 4, &pixel, 4);
} else {
const u16 p16 = (u16)pixel;
memcpy(dest + (y * bufferWidth + x) * 2, &p16, 2);
}
}
}
NotifyMemInfo(MemBlockFlags::WRITE, bufferRGB, destSize, "MpegBaseCsc");
return hleLogDebug(Log::Mpeg, 0, "%dx%d at %d,%d -> %08x stride %d",
rangeWidth, rangeHeight, rangeX, rangeY, bufferRGB, bufferWidth);
}
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");
}
return MpegBaseCscRange(bufferRGB, cscAddr, bufferWidth, 0, 0, csc->width << 4, csc->height << 4);
}
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);
}
// Copies one 48-byte YCrCb descriptor over another. The hardware copies the buffers themselves
// when told to; games seen so far only use it to move the descriptor around, so that is all we
// do until something needs more.
static int sceMpegBaseYCrCbCopy(u32 dstAddr, u32 srcAddr, int flags) {
if (!Memory::IsValidRange(dstAddr, sizeof(SceMp4AvcCscStruct)) ||
!Memory::IsValidRange(srcAddr, sizeof(SceMp4AvcCscStruct))) {
return hleLogError(Log::Mpeg, -1, "bad descriptor pointer");
}
Memory::Memcpy(dstAddr, srcAddr, (u32)sizeof(SceMp4AvcCscStruct), "MpegBaseYCrCbCopy");
return hleLogDebug(Log::Mpeg, 0, "flags %08x - descriptor copied, buffers left alone", flags);
}
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);
};