mirror of
https://github.com/hrydgard/ppsspp.git
synced 2024-11-24 14:00:03 +00:00
c03f6c23ed
Before, we were only returning an error once, in an effort to log only once. This meant sometimes games would not realize they needed to add packets.
2274 lines
75 KiB
C++
2274 lines
75 KiB
C++
// Copyright (c) 2012- 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/.
|
|
|
|
|
|
// This code is part shamelessly "inspired" from JPSCP.
|
|
#include <map>
|
|
#include <algorithm>
|
|
|
|
#include "Core/HLE/sceMpeg.h"
|
|
#include "Core/HLE/sceKernelModule.h"
|
|
#include "Core/HLE/sceKernelThread.h"
|
|
#include "Core/HLE/HLE.h"
|
|
#include "Core/HLE/FunctionWrappers.h"
|
|
#include "Core/HW/MediaEngine.h"
|
|
#include "Core/Config.h"
|
|
#include "Core/MemMapHelpers.h"
|
|
#include "Core/Reporting.h"
|
|
#include "GPU/GPUInterface.h"
|
|
#include "GPU/GPUState.h"
|
|
|
|
// MPEG AVC elementary stream.
|
|
static const int MPEG_AVC_ES_SIZE = 2048; // MPEG packet size.
|
|
|
|
// MPEG ATRAC elementary stream.
|
|
static const int MPEG_ATRAC_ES_SIZE = 2112;
|
|
static const int MPEG_ATRAC_ES_OUTPUT_SIZE = 8192;
|
|
|
|
// MPEG PCM elementary stream.
|
|
static const int MPEG_PCM_ES_SIZE = 320;
|
|
static const int MPEG_PCM_ES_OUTPUT_SIZE = 320;
|
|
|
|
// MPEG Userdata elementary stream.
|
|
static const int MPEG_DATA_ES_SIZE = 0xA0000;
|
|
static const int MPEG_DATA_ES_OUTPUT_SIZE = 0xA0000;
|
|
static const int MPEG_DATA_ES_BUFFERS = 2;
|
|
|
|
// MPEG analysis results.
|
|
static const int MPEG_VERSION_0012 = 0;
|
|
static const int MPEG_VERSION_0013 = 1;
|
|
static const int MPEG_VERSION_0014 = 2;
|
|
static const int MPEG_VERSION_0015 = 3;
|
|
|
|
// PSMF analysis results.
|
|
static const int PSMF_VERSION_0012 = 0x32313030;
|
|
static const int PSMF_VERSION_0013 = 0x33313030;
|
|
static const int PSMF_VERSION_0014 = 0x34313030;
|
|
static const int PSMF_VERSION_0015 = 0x35313030;
|
|
|
|
// MPEG streams.
|
|
static const int MPEG_AVC_STREAM = 0;
|
|
static const int MPEG_ATRAC_STREAM = 1;
|
|
static const int MPEG_PCM_STREAM = 2;
|
|
static const int MPEG_DATA_STREAM = 3; // Arbitrary user defined type. Can represent audio or video.
|
|
static const int MPEG_AUDIO_STREAM = 15;
|
|
static const int MPEG_AU_MODE_DECODE = 0;
|
|
static const int MPEG_AU_MODE_SKIP = 1;
|
|
static const u32 MPEG_MEMSIZE = 0x10000; // 64k.
|
|
static const int MPEG_AVC_DECODE_SUCCESS = 1; // Internal value.
|
|
|
|
static const int atracDecodeDelayMs = 3000;
|
|
static const int avcFirstDelayMs = 3600;
|
|
static const int avcCscDelayMs = 4000;
|
|
static const int avcDecodeDelayMs = 5400; // Varies between 4700 and 6000.
|
|
static const int avcEmptyDelayMs = 320;
|
|
static const int mpegDecodeErrorDelayMs = 100;
|
|
static const int mpegTimestampPerSecond = 90000; // How many MPEG Timestamp units in a second.
|
|
static const int videoTimestampStep = 3003; // Value based on pmfplayer (mpegTimestampPerSecond / 29.970 (fps)).
|
|
static const int audioTimestampStep = 4180; // For audio play at 44100 Hz (2048 samples / 44100 * mpegTimestampPerSecond == 4180)
|
|
static const int audioFirstTimestamp = 90000; // The first MPEG audio AU has always this timestamp
|
|
static const int maxAheadTimestamp = 40000;
|
|
static const s64 UNKNOWN_TIMESTAMP = -1;
|
|
|
|
// At least 2048 bytes of MPEG data is provided when analysing the MPEG header
|
|
static const int MPEG_HEADER_BUFFER_MINIMUM_SIZE = 2048;
|
|
|
|
// For PMP media
|
|
static u32 pmp_videoSource = 0; //pointer to the video source (SceMpegLLi structure)
|
|
static int pmp_nBlocks = 0; //number of blocks received in the last sceMpegbase_BEA18F91 call
|
|
static std::list<AVFrame*> pmp_queue; //list of pmp video frames have been decoded and will be played
|
|
static std::list<u32> pmp_ContextList; //list of pmp media contexts
|
|
static bool pmp_oldStateLoaded = false; // for dostate
|
|
|
|
#ifdef USE_FFMPEG
|
|
|
|
extern "C" {
|
|
#include "libavformat/avformat.h"
|
|
#include "libswscale/swscale.h"
|
|
}
|
|
static AVPixelFormat pmp_want_pix_fmt;
|
|
|
|
#endif
|
|
|
|
struct SceMpegLLI
|
|
{
|
|
u32 pSrc;
|
|
u32 pDst;
|
|
u32 Next;
|
|
int iSize;
|
|
};
|
|
|
|
void SceMpegAu::read(u32 addr) {
|
|
Memory::ReadStruct(addr, this);
|
|
pts = (pts & 0xFFFFFFFFULL) << 32 | (((u64)pts) >> 32);
|
|
dts = (dts & 0xFFFFFFFFULL) << 32 | (((u64)dts) >> 32);
|
|
}
|
|
|
|
void SceMpegAu::write(u32 addr) {
|
|
pts = (pts & 0xFFFFFFFFULL) << 32 | (((u64)pts) >> 32);
|
|
dts = (dts & 0xFFFFFFFFULL) << 32 | (((u64)dts) >> 32);
|
|
Memory::WriteStruct(addr, this);
|
|
}
|
|
|
|
static int getMaxAheadTimestamp(const SceMpegRingBuffer &ringbuf) {
|
|
return std::max(maxAheadTimestamp, 700 * ringbuf.packets); // empiric value from JPCSP, thanks!
|
|
}
|
|
|
|
const u8 defaultMpegheader[2048] = {0x50,0x53,0x4d,0x46,0x30,0x30,0x31,0x35,0x00,0x00,0x08,0x00,0x00,
|
|
0x10,0xc8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x4e,0x00,
|
|
0x00,0x00,0x01,0x5f,0x90,0x00,0x00,0x00,0x0d,0xbe,0xca,0x00,0x00,0x61,0xa8,0x00,0x01,0x5f,
|
|
0x90,0x02,0x01,0x00,0x00,0x00,0x34,0x00,0x00,0x00,0x01,0x5f,0x90,0x00,0x00,0x00,0x0d,0xbe,
|
|
0xca,0x00,0x01,0x00,0x00,0x00,0x22,0x00,0x02,0xe0,0x00,0x20,0xfb,0x00,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x1e,0x11,0x00,0x00,0xbd,0x00,0x20,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x02,0x02};
|
|
|
|
// Internal structure
|
|
struct AvcContext {
|
|
int avcDetailFrameWidth;
|
|
int avcDetailFrameHeight;
|
|
int avcDecodeResult;
|
|
int avcFrameStatus;
|
|
};
|
|
|
|
struct StreamInfo {
|
|
int type;
|
|
int num;
|
|
int sid;
|
|
bool needsReset;
|
|
};
|
|
|
|
typedef std::map<u32, StreamInfo> StreamInfoMap;
|
|
|
|
// Internal structure
|
|
struct MpegContext {
|
|
MpegContext() : mediaengine(nullptr), ringbufferNeedsReverse(false) {
|
|
memcpy(mpegheader, defaultMpegheader, 2048);
|
|
}
|
|
~MpegContext() {
|
|
delete mediaengine;
|
|
}
|
|
|
|
void DoState(PointerWrap &p) {
|
|
auto s = p.Section("MpegContext", 1, 2);
|
|
if (!s)
|
|
return;
|
|
|
|
p.DoArray(mpegheader, 2048);
|
|
p.Do(defaultFrameWidth);
|
|
p.Do(videoFrameCount);
|
|
p.Do(audioFrameCount);
|
|
p.Do(endOfAudioReached);
|
|
p.Do(endOfVideoReached);
|
|
p.Do(videoPixelMode);
|
|
p.Do(mpegMagic);
|
|
p.Do(mpegVersion);
|
|
p.Do(mpegRawVersion);
|
|
p.Do(mpegOffset);
|
|
p.Do(mpegStreamSize);
|
|
p.Do(mpegFirstTimestamp);
|
|
p.Do(mpegLastTimestamp);
|
|
p.Do(mpegFirstDate);
|
|
p.Do(mpegLastDate);
|
|
p.Do(mpegRingbufferAddr);
|
|
p.DoArray(esBuffers, MPEG_DATA_ES_BUFFERS);
|
|
p.Do(avc);
|
|
p.Do(avcRegistered);
|
|
p.Do(atracRegistered);
|
|
p.Do(pcmRegistered);
|
|
p.Do(dataRegistered);
|
|
p.Do(ignoreAtrac);
|
|
p.Do(ignorePcm);
|
|
p.Do(ignoreAvc);
|
|
p.Do(isAnalyzed);
|
|
p.Do<u32, StreamInfo>(streamMap);
|
|
p.DoClass(mediaengine);
|
|
ringbufferNeedsReverse = s < 2;
|
|
}
|
|
|
|
u8 mpegheader[2048];
|
|
u32 defaultFrameWidth;
|
|
int videoFrameCount;
|
|
int audioFrameCount;
|
|
bool endOfAudioReached;
|
|
bool endOfVideoReached;
|
|
int videoPixelMode;
|
|
u32 mpegMagic;
|
|
int mpegVersion;
|
|
u32 mpegRawVersion;
|
|
u32 mpegOffset;
|
|
u32 mpegStreamSize;
|
|
s64 mpegFirstTimestamp;
|
|
s64 mpegLastTimestamp;
|
|
u32 mpegFirstDate;
|
|
u32 mpegLastDate;
|
|
u32 mpegRingbufferAddr;
|
|
bool esBuffers[MPEG_DATA_ES_BUFFERS];
|
|
AvcContext avc;
|
|
|
|
bool avcRegistered;
|
|
bool atracRegistered;
|
|
bool pcmRegistered;
|
|
bool dataRegistered;
|
|
|
|
bool ignoreAtrac;
|
|
bool ignorePcm;
|
|
bool ignoreAvc;
|
|
|
|
bool isAnalyzed;
|
|
bool ringbufferNeedsReverse;
|
|
|
|
StreamInfoMap streamMap;
|
|
MediaEngine *mediaengine;
|
|
};
|
|
|
|
static bool isMpegInit;
|
|
static int mpegLibVersion;
|
|
static u32 streamIdGen;
|
|
static int actionPostPut;
|
|
static std::map<u32, MpegContext *> mpegMap;
|
|
|
|
static MpegContext *getMpegCtx(u32 mpegAddr) {
|
|
if (!Memory::IsValidAddress(mpegAddr))
|
|
return nullptr;
|
|
|
|
u32 mpeg = Memory::Read_U32(mpegAddr);
|
|
auto found = mpegMap.find(mpeg);
|
|
if (found == mpegMap.end())
|
|
return nullptr;
|
|
|
|
MpegContext *res = found->second;
|
|
// Take this opportunity to upgrade savestates if necessary.
|
|
if (res->ringbufferNeedsReverse) {
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(res->mpegRingbufferAddr);
|
|
ringbuffer->packetsAvail = ringbuffer->packets - ringbuffer->packetsAvail;
|
|
res->ringbufferNeedsReverse = false;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static void InitRingbuffer(SceMpegRingBuffer *buf, int packets, int data, int size, int callback_addr, int callback_args) {
|
|
buf->packets = packets;
|
|
buf->packetsRead = 0;
|
|
buf->packetsWritten = 0;
|
|
buf->packetsAvail = 0;
|
|
buf->packetSize = 2048;
|
|
buf->data = data;
|
|
buf->callback_addr = callback_addr;
|
|
buf->callback_args = callback_args;
|
|
buf->dataUpperBound = data + packets * 2048;
|
|
buf->semaID = 0;
|
|
buf->mpeg = 0;
|
|
// This isn't in ver 0104, but it is in 0105.
|
|
if (mpegLibVersion >= 0x0105)
|
|
buf->gp = __KernelGetModuleGP(__KernelGetCurThreadModuleId());
|
|
}
|
|
|
|
static u32 convertTimestampToDate(u32 ts) {
|
|
return ts; // TODO
|
|
}
|
|
|
|
static u32 getMpegVersion(u32 mpegRawVersion) {
|
|
switch (mpegRawVersion) {
|
|
case PSMF_VERSION_0012: return MPEG_VERSION_0012;
|
|
case PSMF_VERSION_0013: return MPEG_VERSION_0013;
|
|
case PSMF_VERSION_0014: return MPEG_VERSION_0014;
|
|
case PSMF_VERSION_0015: return MPEG_VERSION_0015;
|
|
default: return -1;
|
|
}
|
|
}
|
|
static void AnalyzeMpeg(u8 *buffer, MpegContext *ctx) {
|
|
ctx->mpegMagic = *(u32_le*)buffer;
|
|
ctx->mpegRawVersion = *(u32_le*)(buffer + PSMF_STREAM_VERSION_OFFSET);
|
|
ctx->mpegVersion = getMpegVersion(ctx->mpegRawVersion);
|
|
ctx->mpegOffset = bswap32(*(u32_le*)(buffer + PSMF_STREAM_OFFSET_OFFSET));
|
|
ctx->mpegStreamSize = bswap32(*(u32_le*)(buffer + PSMF_STREAM_SIZE_OFFSET));
|
|
ctx->mpegFirstTimestamp = getMpegTimeStamp(buffer + PSMF_FIRST_TIMESTAMP_OFFSET);
|
|
ctx->mpegLastTimestamp = getMpegTimeStamp(buffer + PSMF_LAST_TIMESTAMP_OFFSET);
|
|
ctx->mpegFirstDate = convertTimestampToDate(ctx->mpegFirstTimestamp);
|
|
ctx->mpegLastDate = convertTimestampToDate(ctx->mpegLastTimestamp);
|
|
ctx->avc.avcDetailFrameWidth = (*(u8*)(buffer + 142)) * 0x10;
|
|
ctx->avc.avcDetailFrameHeight = (*(u8*)(buffer + 143)) * 0x10;
|
|
ctx->avc.avcDecodeResult = MPEG_AVC_DECODE_SUCCESS;
|
|
ctx->avc.avcFrameStatus = 0;
|
|
ctx->videoFrameCount = 0;
|
|
ctx->audioFrameCount = 0;
|
|
ctx->endOfAudioReached = false;
|
|
ctx->endOfVideoReached = false;
|
|
|
|
// Sanity Check ctx->mpegFirstTimestamp
|
|
if (ctx->mpegFirstTimestamp != 90000) {
|
|
WARN_LOG_REPORT(ME, "Unexpected mpeg first timestamp: %llx / %lld", ctx->mpegFirstTimestamp, ctx->mpegFirstTimestamp);
|
|
}
|
|
|
|
if (ctx->mpegMagic != PSMF_MAGIC || ctx->mpegVersion < 0 ||
|
|
(ctx->mpegOffset & 2047) != 0 || ctx->mpegOffset == 0) {
|
|
// mpeg header is invalid!
|
|
return;
|
|
}
|
|
|
|
if (ctx->mediaengine && (ctx->mpegStreamSize > 0) && !ctx->isAnalyzed) {
|
|
// init mediaEngine
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ctx->mpegRingbufferAddr);
|
|
if (ringbuffer.IsValid()) {
|
|
ctx->mediaengine->loadStream(buffer, ctx->mpegOffset, ringbuffer->packets * ringbuffer->packetSize);
|
|
} else {
|
|
// TODO: Does this make any sense?
|
|
ctx->mediaengine->loadStream(buffer, ctx->mpegOffset, 0);
|
|
}
|
|
ctx->mediaengine->setVideoDim();
|
|
}
|
|
|
|
// When used with scePsmf, some applications attempt to use sceMpegQueryStreamOffset
|
|
// and sceMpegQueryStreamSize, which forces a packet overwrite in the Media Engine and in
|
|
// the MPEG ringbuffer.
|
|
// Mark the current MPEG as analyzed to filter this, and restore it at sceMpegFinish.
|
|
ctx->isAnalyzed = true;
|
|
|
|
// copy header struct to mpeg header.
|
|
memcpy(ctx->mpegheader, buffer, 2048);
|
|
*(u32_le*)(ctx->mpegheader + PSMF_STREAM_OFFSET_OFFSET) = 0x80000;
|
|
|
|
INFO_LOG(ME, "Stream offset: %d, Stream size: 0x%X", ctx->mpegOffset, ctx->mpegStreamSize);
|
|
INFO_LOG(ME, "First timestamp: %lld, Last timestamp: %lld", ctx->mpegFirstTimestamp, ctx->mpegLastTimestamp);
|
|
}
|
|
|
|
class PostPutAction : public Action {
|
|
public:
|
|
PostPutAction() {}
|
|
void setRingAddr(u32 ringAddr) { ringAddr_ = ringAddr; }
|
|
static Action *Create() { return new PostPutAction; }
|
|
void DoState(PointerWrap &p) override {
|
|
auto s = p.Section("PostPutAction", 1);
|
|
if (!s)
|
|
return;
|
|
|
|
p.Do(ringAddr_);
|
|
}
|
|
void run(MipsCall &call) override;
|
|
private:
|
|
u32 ringAddr_;
|
|
};
|
|
|
|
void __MpegInit() {
|
|
isMpegInit = false;
|
|
mpegLibVersion = 0x010A;
|
|
streamIdGen = 1;
|
|
actionPostPut = __KernelRegisterActionType(PostPutAction::Create);
|
|
|
|
#ifdef USE_FFMPEG
|
|
avcodec_register_all();
|
|
av_register_all();
|
|
#endif
|
|
}
|
|
|
|
void __MpegDoState(PointerWrap &p) {
|
|
auto s = p.Section("sceMpeg", 1, 2);
|
|
if (!s)
|
|
return;
|
|
|
|
if (s < 2) {
|
|
int oldLastMpeg = -1;
|
|
bool oldIsMpegAnalyzed = false;
|
|
p.Do(oldLastMpeg);
|
|
p.Do(streamIdGen);
|
|
p.Do(oldIsMpegAnalyzed);
|
|
// Let's assume the oldest version.
|
|
mpegLibVersion = 0x0101;
|
|
} else {
|
|
p.Do(streamIdGen);
|
|
p.Do(mpegLibVersion);
|
|
}
|
|
p.Do(isMpegInit);
|
|
p.Do(actionPostPut);
|
|
__KernelRestoreActionType(actionPostPut, PostPutAction::Create);
|
|
|
|
p.Do(mpegMap);
|
|
}
|
|
|
|
void __MpegShutdown() {
|
|
std::map<u32, MpegContext *>::iterator it, end;
|
|
for (it = mpegMap.begin(), end = mpegMap.end(); it != end; ++it) {
|
|
delete it->second;
|
|
}
|
|
mpegMap.clear();
|
|
}
|
|
|
|
void __MpegLoadModule(int version) {
|
|
mpegLibVersion = version;
|
|
}
|
|
|
|
static u32 sceMpegInit() {
|
|
if (isMpegInit) {
|
|
WARN_LOG(ME, "sceMpegInit(): already initialized");
|
|
// TODO: Need to properly hook module load/unload for this to work right.
|
|
//return ERROR_MPEG_ALREADY_INIT;
|
|
} else {
|
|
INFO_LOG(ME, "sceMpegInit()");
|
|
}
|
|
isMpegInit = true;
|
|
return hleDelayResult(0, "mpeg init", 750);
|
|
}
|
|
|
|
static u32 __MpegRingbufferQueryMemSize(int packets) {
|
|
return packets * (104 + 2048);
|
|
}
|
|
|
|
static u32 sceMpegRingbufferQueryMemSize(int packets) {
|
|
u32 size = __MpegRingbufferQueryMemSize(packets);
|
|
DEBUG_LOG(ME, "%i = sceMpegRingbufferQueryMemSize(%i)", size, packets);
|
|
return size;
|
|
}
|
|
|
|
|
|
static u32 sceMpegRingbufferConstruct(u32 ringbufferAddr, u32 numPackets, u32 data, u32 size, u32 callbackAddr, u32 callbackArg) {
|
|
if (!Memory::IsValidAddress(ringbufferAddr)) {
|
|
ERROR_LOG_REPORT(ME, "sceMpegRingbufferConstruct(%08x, %i, %08x, %08x, %08x, %08x): bad ringbuffer, should crash", ringbufferAddr, numPackets, data, size, callbackAddr, callbackArg);
|
|
return SCE_KERNEL_ERROR_ILLEGAL_ADDRESS;
|
|
}
|
|
|
|
if ((int)size < 0) {
|
|
ERROR_LOG_REPORT(ME, "sceMpegRingbufferConstruct(%08x, %i, %08x, %08x, %08x, %08x): invalid size", ringbufferAddr, numPackets, data, size, callbackAddr, callbackArg);
|
|
return ERROR_MPEG_NO_MEMORY;
|
|
}
|
|
|
|
if (__MpegRingbufferQueryMemSize(numPackets) > size) {
|
|
if (numPackets < 0x00100000) {
|
|
ERROR_LOG_REPORT(ME, "sceMpegRingbufferConstruct(%08x, %i, %08x, %08x, %08x, %08x): too many packets for buffer", ringbufferAddr, numPackets, data, size, callbackAddr, callbackArg);
|
|
return ERROR_MPEG_NO_MEMORY;
|
|
} else {
|
|
// The PSP's firmware allows some cases here, due to a bug in its validation.
|
|
ERROR_LOG_REPORT(ME, "sceMpegRingbufferConstruct(%08x, %i, %08x, %08x, %08x, %08x): too many packets for buffer, bogus size", ringbufferAddr, numPackets, data, size, callbackAddr, callbackArg);
|
|
}
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegRingbufferConstruct(%08x, %i, %08x, %08x, %08x, %08x)", ringbufferAddr, numPackets, data, size, callbackAddr, callbackArg);
|
|
auto ring = PSPPointer<SceMpegRingBuffer>::Create(ringbufferAddr);
|
|
InitRingbuffer(ring, numPackets, data, size, callbackAddr, callbackArg);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegCreate(u32 mpegAddr, u32 dataPtr, u32 size, u32 ringbufferAddr, u32 frameWidth, u32 mode, u32 ddrTop)
|
|
{
|
|
if (!Memory::IsValidAddress(mpegAddr)) {
|
|
WARN_LOG(ME, "sceMpegCreate(%08x, %08x, %i, %08x, %i, %i, %i): invalid addresses", mpegAddr, dataPtr, size, ringbufferAddr, frameWidth, mode, ddrTop);
|
|
return -1;
|
|
}
|
|
|
|
if (size < MPEG_MEMSIZE) {
|
|
WARN_LOG(ME, "ERROR_MPEG_NO_MEMORY=sceMpegCreate(%08x, %08x, %i, %08x, %i, %i, %i)", mpegAddr, dataPtr, size, ringbufferAddr, frameWidth, mode, ddrTop);
|
|
return ERROR_MPEG_NO_MEMORY;
|
|
}
|
|
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ringbufferAddr);
|
|
if (ringbuffer.IsValid()) {
|
|
if (ringbuffer->packetSize == 0) {
|
|
ringbuffer->packetsAvail = 0;
|
|
} else {
|
|
ringbuffer->packetsAvail = ringbuffer->packets - (ringbuffer->dataUpperBound - ringbuffer->data) / ringbuffer->packetSize;
|
|
}
|
|
ringbuffer->mpeg = mpegAddr;
|
|
}
|
|
|
|
// Generate, and write mpeg handle into mpeg data, for some reason
|
|
int mpegHandle = dataPtr + 0x30;
|
|
Memory::Write_U32(mpegHandle, mpegAddr);
|
|
|
|
// Initialize fake mpeg struct.
|
|
Memory::Memcpy(mpegHandle, "LIBMPEG\0", 8);
|
|
Memory::Memcpy(mpegHandle + 8, "001\0", 4);
|
|
Memory::Write_U32(-1, mpegHandle + 12);
|
|
if (ringbuffer.IsValid()) {
|
|
Memory::Write_U32(ringbufferAddr, mpegHandle + 16);
|
|
Memory::Write_U32(ringbuffer->dataUpperBound, mpegHandle + 20);
|
|
}
|
|
MpegContext *ctx = new MpegContext;
|
|
if (mpegMap.find(mpegHandle) != mpegMap.end()) {
|
|
WARN_LOG_REPORT(HLE, "Replacing existing mpeg context at %08x", mpegAddr);
|
|
// Otherwise, it would leak.
|
|
delete mpegMap[mpegHandle];
|
|
}
|
|
mpegMap[mpegHandle] = ctx;
|
|
|
|
// Initialize mpeg values.
|
|
ctx->mpegRingbufferAddr = ringbufferAddr;
|
|
ctx->videoFrameCount = 0;
|
|
ctx->audioFrameCount = 0;
|
|
ctx->videoPixelMode = GE_CMODE_32BIT_ABGR8888; // TODO: What's the actual default?
|
|
ctx->avcRegistered = false;
|
|
ctx->atracRegistered = false;
|
|
ctx->pcmRegistered = false;
|
|
ctx->dataRegistered = false;
|
|
ctx->ignoreAtrac = false;
|
|
ctx->ignorePcm = false;
|
|
ctx->ignoreAvc = false;
|
|
ctx->defaultFrameWidth = frameWidth;
|
|
for (int i = 0; i < MPEG_DATA_ES_BUFFERS; i++) {
|
|
ctx->esBuffers[i] = false;
|
|
}
|
|
|
|
// Detailed "analysis" is done later in Query* for some reason.
|
|
ctx->isAnalyzed = false;
|
|
ctx->mediaengine = new MediaEngine();
|
|
|
|
INFO_LOG(ME, "%08x=sceMpegCreate(%08x, %08x, %i, %08x, %i, %i, %i)", mpegHandle, mpegAddr, dataPtr, size, ringbufferAddr, frameWidth, mode, ddrTop);
|
|
return hleDelayResult(0, "mpeg create", 29000);
|
|
}
|
|
|
|
static int sceMpegDelete(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegDelete(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegDelete(%08x)", mpeg);
|
|
|
|
delete ctx;
|
|
mpegMap.erase(Memory::Read_U32(mpeg));
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int sceMpegAvcDecodeMode(u32 mpeg, u32 modeAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(modeAddr)) {
|
|
WARN_LOG(ME, "sceMpegAvcDecodeMode(%08x, %08x): invalid addresses", mpeg, modeAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegAvcDecodeMode(%08x, %08x): bad mpeg handle", mpeg, modeAddr);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegAvcDecodeMode(%08x, %08x)", mpeg, modeAddr);
|
|
|
|
int mode = Memory::Read_U32(modeAddr);
|
|
int pixelMode = Memory::Read_U32(modeAddr + 4);
|
|
if (pixelMode >= GE_CMODE_16BIT_BGR5650 && pixelMode <= GE_CMODE_32BIT_ABGR8888) {
|
|
ctx->videoPixelMode = pixelMode;
|
|
} else {
|
|
ERROR_LOG(ME, "sceMpegAvcDecodeMode(%i, %i): unknown pixelMode ", mode, pixelMode);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegQueryStreamOffset(u32 mpeg, u32 bufferAddr, u32 offsetAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(bufferAddr) || !Memory::IsValidAddress(offsetAddr)) {
|
|
ERROR_LOG(ME, "sceMpegQueryStreamOffset(%08x, %08x, %08x): invalid addresses", mpeg, bufferAddr, offsetAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegQueryStreamOffset(%08x, %08x, %08x): bad mpeg handle", mpeg, bufferAddr, offsetAddr);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegQueryStreamOffset(%08x, %08x, %08x)", mpeg, bufferAddr, offsetAddr);
|
|
|
|
// Kinda destructive, no?
|
|
AnalyzeMpeg(Memory::GetPointer(bufferAddr), ctx);
|
|
|
|
if (ctx->mpegMagic != PSMF_MAGIC) {
|
|
ERROR_LOG(ME, "sceMpegQueryStreamOffset: Bad PSMF magic");
|
|
Memory::Write_U32(0, offsetAddr);
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
} else if (ctx->mpegVersion < 0) {
|
|
ERROR_LOG(ME, "sceMpegQueryStreamOffset: Bad version");
|
|
Memory::Write_U32(0, offsetAddr);
|
|
return ERROR_MPEG_BAD_VERSION;
|
|
} else if ((ctx->mpegOffset & 2047) != 0 || ctx->mpegOffset == 0) {
|
|
ERROR_LOG(ME, "sceMpegQueryStreamOffset: Bad offset");
|
|
Memory::Write_U32(0, offsetAddr);
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
}
|
|
|
|
Memory::Write_U32(ctx->mpegOffset, offsetAddr);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegQueryStreamSize(u32 bufferAddr, u32 sizeAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(bufferAddr) || !Memory::IsValidAddress(sizeAddr)) {
|
|
ERROR_LOG(ME, "sceMpegQueryStreamSize(%08x, %08x): invalid addresses", bufferAddr, sizeAddr);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegQueryStreamSize(%08x, %08x)", bufferAddr, sizeAddr);
|
|
|
|
MpegContext ctx;
|
|
ctx.mediaengine = 0;
|
|
|
|
AnalyzeMpeg(Memory::GetPointer(bufferAddr), &ctx);
|
|
|
|
if (ctx.mpegMagic != PSMF_MAGIC) {
|
|
ERROR_LOG(ME, "sceMpegQueryStreamSize: Bad PSMF magic");
|
|
Memory::Write_U32(0, sizeAddr);
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
} else if ((ctx.mpegOffset & 2047) != 0 ) {
|
|
ERROR_LOG(ME, "sceMpegQueryStreamSize: Bad offset");
|
|
Memory::Write_U32(0, sizeAddr);
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
}
|
|
|
|
Memory::Write_U32(ctx.mpegStreamSize, sizeAddr);
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegRegistStream(u32 mpeg, u32 streamType, u32 streamNum)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegRegistStream(%08x, %i, %i): bad mpeg handle", mpeg, streamType, streamNum);
|
|
return -1;
|
|
}
|
|
|
|
INFO_LOG(ME, "sceMpegRegistStream(%08x, %i, %i)", mpeg, streamType, streamNum);
|
|
|
|
switch (streamType) {
|
|
case MPEG_AVC_STREAM:
|
|
ctx->avcRegistered = true;
|
|
// TODO: Probably incorrect?
|
|
ctx->mediaengine->setVideoStream(streamNum);
|
|
break;
|
|
case MPEG_AUDIO_STREAM:
|
|
case MPEG_ATRAC_STREAM:
|
|
ctx->atracRegistered = true;
|
|
// TODO: Probably incorrect?
|
|
ctx->mediaengine->setAudioStream(streamNum);
|
|
break;
|
|
case MPEG_PCM_STREAM:
|
|
ctx->pcmRegistered = true;
|
|
break;
|
|
case MPEG_DATA_STREAM:
|
|
ctx->dataRegistered = true;
|
|
break;
|
|
default :
|
|
DEBUG_LOG(ME, "sceMpegRegistStream(%i) : unknown stream type", streamType);
|
|
break;
|
|
}
|
|
// ...
|
|
u32 sid = streamIdGen++;
|
|
StreamInfo info;
|
|
info.type = streamType;
|
|
info.num = streamNum;
|
|
info.sid = sid;
|
|
info.needsReset = true;
|
|
ctx->streamMap[sid] = info;
|
|
return sid;
|
|
}
|
|
|
|
static int sceMpegMallocAvcEsBuf(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegMallocAvcEsBuf(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegMallocAvcEsBuf(%08x)", mpeg);
|
|
|
|
// Doesn't actually malloc, just keeps track of a couple of flags
|
|
for (int i = 0; i < MPEG_DATA_ES_BUFFERS; i++) {
|
|
if (!ctx->esBuffers[i]) {
|
|
ctx->esBuffers[i] = true;
|
|
return i + 1;
|
|
}
|
|
}
|
|
// No es buffer
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegFreeAvcEsBuf(u32 mpeg, int esBuf)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegFreeAvcEsBuf(%08x, %i): bad mpeg handle", mpeg, esBuf);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegFreeAvcEsBuf(%08x, %i)", mpeg, esBuf);
|
|
|
|
if (esBuf == 0) {
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
}
|
|
|
|
if (esBuf >= 1 && esBuf <= MPEG_DATA_ES_BUFFERS) {
|
|
// TODO: Check if it's already been free'd?
|
|
ctx->esBuffers[esBuf - 1] = false;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// this function is used for dumping a video frame into a file.
|
|
// you can use it when you want to output the decoded frame.
|
|
static void SaveFrame(AVFrame *pFrame, int width, int height)
|
|
{
|
|
FILE *pFile;
|
|
char szFilename[] = "frame.ppm";
|
|
int y;
|
|
|
|
pFile = fopen(szFilename, "wb");
|
|
if (!pFile)
|
|
return;
|
|
// width * 4 is for 32-bit RGBA format.
|
|
// You could change to your desired format as width*(4:32-bit,3:24-bit, 2:16-bit,1:8-bit)
|
|
for (y = 0; y < height; y++)
|
|
fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 4, pFile);
|
|
|
|
fclose(pFile);
|
|
}
|
|
|
|
// check the existence of pmp media context
|
|
static bool isContextExist(u32 ctxAddr){
|
|
for (auto it = pmp_ContextList.begin(); it != pmp_ContextList.end(); ++it){
|
|
if (*it == ctxAddr){
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Initialize Pmp video parameters and decoder.
|
|
static bool InitPmp(MpegContext * ctx){
|
|
#ifdef USE_FFMPEG
|
|
InitFFmpeg();
|
|
auto mediaengine = ctx->mediaengine;
|
|
mediaengine->m_isVideoEnd = false;
|
|
mediaengine->m_firstTimeStamp = 0;
|
|
mediaengine->m_lastTimeStamp = 0;
|
|
ctx->mpegFirstTimestamp = 0;
|
|
ctx->mpegLastTimestamp = 0;
|
|
|
|
// wanted output pixel format
|
|
// reference values for pix_fmt:
|
|
// GE_CMODE_16BIT_BGR5650 <--> AV_PIX_FMT_BGR565LE
|
|
// GE_CMODE_16BIT_ABGR5551 <--> AV_PIX_FMT_BGR555LE;
|
|
// GE_CMODE_16BIT_ABGR4444 <--> AV_PIX_FMT_BGR444LE;
|
|
// GE_CMODE_32BIT_ABGR8888 <--> AV_PIX_FMT_RGBA;
|
|
pmp_want_pix_fmt = AV_PIX_FMT_RGBA;
|
|
|
|
// Create H264 video codec
|
|
AVCodec * pmp_Codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
|
if (pmp_Codec == NULL){
|
|
ERROR_LOG(ME, "Can not find H264 codec, please update ffmpeg");
|
|
return false;
|
|
}
|
|
|
|
// Create CodecContext
|
|
AVCodecContext * pmp_CodecCtx = avcodec_alloc_context3(pmp_Codec);
|
|
if (pmp_CodecCtx == NULL){
|
|
ERROR_LOG(ME, "Can not allocate pmp Codec Context");
|
|
return false;
|
|
}
|
|
|
|
// each pmp video context is corresponding to one pmp video codec
|
|
mediaengine->m_pCodecCtxs[0] = pmp_CodecCtx;
|
|
|
|
// initialize H264 video parameters
|
|
// set pmp video size. Better to get from pmp file directly if possible. Any idea?
|
|
pmp_CodecCtx->width = 480;
|
|
pmp_CodecCtx->height = 272;
|
|
mediaengine->m_desHeight = pmp_CodecCtx->height;
|
|
mediaengine->m_desWidth = pmp_CodecCtx->width;
|
|
|
|
// Open pmp video codec
|
|
if (avcodec_open2(pmp_CodecCtx, pmp_Codec, NULL) < 0){
|
|
ERROR_LOG(ME, "Can not open pmp video codec");
|
|
return false;
|
|
}
|
|
|
|
// initialize ctx->mediaengine->m_pFrame and ctx->mediaengine->m_pFrameRGB
|
|
if (!mediaengine->m_pFrame){
|
|
mediaengine->m_pFrame = av_frame_alloc();
|
|
}
|
|
if (!mediaengine->m_pFrameRGB){
|
|
mediaengine->m_pFrameRGB = av_frame_alloc();
|
|
}
|
|
|
|
// get RGBA picture buffer
|
|
mediaengine->m_bufSize = avpicture_get_size(pmp_want_pix_fmt, pmp_CodecCtx->width, pmp_CodecCtx->height);
|
|
mediaengine->m_buffer = (u8*)av_malloc(mediaengine->m_bufSize);
|
|
|
|
return true;
|
|
#else
|
|
// we can not play pmp video without ffmpeg
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
// This class H264Frames is used for collecting small pieces of frames into larger frames for ffmpeg to decode
|
|
// Basically, this will avoid incomplete frame decoding issue and improve much better the video quality.
|
|
class H264Frames{
|
|
public:
|
|
int size;
|
|
u8* stream;
|
|
|
|
H264Frames() :size(0), stream(NULL){};
|
|
|
|
H264Frames(u8* str, int sz) :size(sz){
|
|
stream = new u8[size];
|
|
memcpy(stream, str, size);
|
|
};
|
|
|
|
H264Frames(H264Frames *frame){
|
|
size = frame->size;
|
|
stream = new u8[size];
|
|
memcpy(stream, frame->stream, size);
|
|
};
|
|
|
|
~H264Frames(){
|
|
size = 0;
|
|
if (stream){
|
|
delete[] stream;
|
|
stream = NULL;
|
|
}
|
|
};
|
|
|
|
void add(H264Frames *p){
|
|
add(p->stream, p->size);
|
|
};
|
|
|
|
void add(u8* str, int sz){
|
|
int newsize = size + sz;
|
|
u8* newstream = new u8[newsize];
|
|
// join two streams
|
|
memcpy(newstream, stream, size);
|
|
memcpy(newstream + size, str, sz);
|
|
// delete old stream
|
|
delete[] stream;
|
|
// replace with new stream
|
|
stream = newstream;
|
|
size = newsize;
|
|
};
|
|
|
|
void remove(int pos){
|
|
// remove stream from begining to pos
|
|
if (pos == 0){
|
|
// nothing to remove
|
|
}
|
|
else if (pos >= size){
|
|
// we remove all
|
|
size = 0;
|
|
if (stream){
|
|
delete[] stream;
|
|
stream = NULL;
|
|
}
|
|
}
|
|
else{
|
|
// we remove the front part
|
|
size -= pos;
|
|
u8* str = new u8[size];
|
|
memcpy(str, stream + pos, size);
|
|
delete[] stream;
|
|
stream = str;
|
|
}
|
|
};
|
|
#ifndef USE_FFMPEG
|
|
#define FF_INPUT_BUFFER_PADDING_SIZE 16
|
|
#endif
|
|
void addpadding(){
|
|
u8* str = new u8[size + FF_INPUT_BUFFER_PADDING_SIZE];
|
|
memcpy(str, stream, size);
|
|
memset(str + size, 0, FF_INPUT_BUFFER_PADDING_SIZE);
|
|
size += FF_INPUT_BUFFER_PADDING_SIZE;
|
|
delete[] stream;
|
|
stream = str;
|
|
}
|
|
};
|
|
|
|
// collect pmp video frames
|
|
static H264Frames *pmpframes;
|
|
|
|
// decode pmp video to RGBA format
|
|
static bool decodePmpVideo(PSPPointer<SceMpegRingBuffer> ringbuffer, u32 pmpctxAddr){
|
|
// the current video is pmp iff pmp_videoSource is a valid addresse
|
|
MpegContext* ctx = getMpegCtx(pmpctxAddr);
|
|
if (Memory::IsValidAddress(pmp_videoSource)){
|
|
// We should initialize pmp codec for each pmp context
|
|
if (isContextExist(pmpctxAddr) == false){
|
|
bool ret = InitPmp(ctx);
|
|
if (!ret){
|
|
ERROR_LOG(ME, "Pmp video initialization failed");
|
|
return false;
|
|
}
|
|
// add the initialized context into ContextList
|
|
pmp_ContextList.push_front(pmpctxAddr);
|
|
}
|
|
|
|
ringbuffer->packetsRead = pmp_nBlocks;
|
|
|
|
MediaEngine* mediaengine = ctx->mediaengine;
|
|
AVFrame* pFrame = mediaengine->m_pFrame;
|
|
AVFrame* pFrameRGB = mediaengine->m_pFrameRGB;
|
|
auto pCodecCtx = mediaengine->m_pCodecCtxs[0];
|
|
|
|
// pmpframes could be destroied when close a video to load another one
|
|
if (!pmpframes)
|
|
pmpframes = new H264Frames;
|
|
|
|
// joint all blocks into H264Frames
|
|
SceMpegLLI lli;
|
|
for (int i = 0; i < pmp_nBlocks; i++){
|
|
Memory::ReadStructUnchecked(pmp_videoSource, &lli);
|
|
// add source block into pmpframes
|
|
pmpframes->add(Memory::GetPointer(lli.pSrc), lli.iSize);
|
|
// get next block
|
|
pmp_videoSource += sizeof(SceMpegLLI);
|
|
}
|
|
|
|
pmpframes->addpadding();
|
|
|
|
// initialize packet
|
|
AVPacket packet;
|
|
av_new_packet(&packet, pCodecCtx->width*pCodecCtx->height);
|
|
|
|
// set packet to source block
|
|
packet.data = pmpframes->stream;
|
|
packet.size = pmpframes->size;
|
|
|
|
// reuse pFrame and pFrameRGB
|
|
int got_picture = 0;
|
|
av_frame_unref(pFrame);
|
|
av_frame_unref(pFrameRGB);
|
|
|
|
// hook pFrameRGB output to buffer
|
|
avpicture_fill((AVPicture *)pFrameRGB, mediaengine->m_buffer, pmp_want_pix_fmt, pCodecCtx->width, pCodecCtx->height);
|
|
|
|
// decode video frame
|
|
int len = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet);
|
|
DEBUG_LOG(ME, "got_picture %d", got_picture);
|
|
if (got_picture){
|
|
SwsContext *img_convert_ctx = NULL;
|
|
img_convert_ctx = sws_getContext(
|
|
pCodecCtx->width,
|
|
pCodecCtx->height,
|
|
pCodecCtx->pix_fmt,
|
|
pCodecCtx->width,
|
|
pCodecCtx->height,
|
|
pmp_want_pix_fmt,
|
|
SWS_BILINEAR,
|
|
NULL, NULL, NULL);
|
|
|
|
if (!img_convert_ctx) {
|
|
ERROR_LOG(ME, "Cannot initialize sws conversion context");
|
|
return false;
|
|
}
|
|
|
|
// Convert to RGBA
|
|
int swsRet = sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data,
|
|
pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
|
|
if (swsRet < 0){
|
|
ERROR_LOG(ME, "sws_scale: Error while converting %d", swsRet);
|
|
return false;
|
|
}
|
|
// free sws context
|
|
sws_freeContext(img_convert_ctx);
|
|
|
|
// update timestamp
|
|
if (av_frame_get_best_effort_timestamp(mediaengine->m_pFrame) != AV_NOPTS_VALUE)
|
|
mediaengine->m_videopts = av_frame_get_best_effort_timestamp(mediaengine->m_pFrame) + av_frame_get_pkt_duration(mediaengine->m_pFrame) - mediaengine->m_firstTimeStamp;
|
|
else
|
|
mediaengine->m_videopts += av_frame_get_pkt_duration(mediaengine->m_pFrame);
|
|
|
|
// push the decoded frame into pmp_queue
|
|
pmp_queue.push_back(pFrameRGB);
|
|
|
|
// write frame into ppm file
|
|
// SaveFrame(pNewFrameRGB, pCodecCtx->width, pCodecCtx->height);
|
|
}
|
|
// free some pointers
|
|
av_free_packet(&packet);
|
|
pmpframes->~H264Frames();
|
|
// must reset pmp_VideoSource address to zero after decoding.
|
|
pmp_videoSource = 0;
|
|
return true;
|
|
}
|
|
// not a pmp video, return false
|
|
return false;
|
|
}
|
|
|
|
|
|
void __VideoPmpInit() {
|
|
pmp_oldStateLoaded = false;
|
|
pmpframes = new H264Frames();
|
|
}
|
|
|
|
void __VideoPmpShutdown() {
|
|
#ifdef USE_FFMPEG
|
|
// We need to empty pmp_queue to not leak memory.
|
|
for (auto it = pmp_queue.begin(); it != pmp_queue.end(); ++it){
|
|
av_free(*it);
|
|
}
|
|
pmp_queue.clear();
|
|
pmp_ContextList.clear();
|
|
delete pmpframes;
|
|
pmpframes = NULL;
|
|
#endif
|
|
}
|
|
|
|
void __VideoPmpDoState(PointerWrap &p){
|
|
auto s = p.Section("PMPVideo", 0, 1);
|
|
if (!s) {
|
|
if (p.mode == PointerWrap::MODE_READ)
|
|
pmp_oldStateLoaded = true;
|
|
return;
|
|
}
|
|
|
|
p.Do(pmp_videoSource);
|
|
p.Do(pmp_nBlocks);
|
|
if (p.mode == PointerWrap::MODE_READ){
|
|
// for loadstate, we will reinitialize the pmp codec
|
|
__VideoPmpShutdown();
|
|
}
|
|
}
|
|
|
|
static u32 sceMpegAvcDecode(u32 mpeg, u32 auAddr, u32 frameWidth, u32 bufferAddr, u32 initAddr)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegAvcDecode(%08x, %08x, %d, %08x, %08x): bad mpeg handle", mpeg, auAddr, frameWidth, bufferAddr, initAddr);
|
|
return -1;
|
|
}
|
|
|
|
if (frameWidth == 0) { // wtf, go sudoku passes in 0xcccccccc
|
|
if (!ctx->defaultFrameWidth) {
|
|
frameWidth = ctx->avc.avcDetailFrameWidth;
|
|
} else {
|
|
frameWidth = ctx->defaultFrameWidth;
|
|
}
|
|
}
|
|
|
|
SceMpegAu avcAu;
|
|
avcAu.read(auAddr);
|
|
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ctx->mpegRingbufferAddr);
|
|
if (!ringbuffer.IsValid()) {
|
|
ERROR_LOG(ME, "Bogus mpegringbufferaddr");
|
|
return -1;
|
|
}
|
|
|
|
u32 buffer = Memory::Read_U32(bufferAddr);
|
|
u32 init = Memory::Read_U32(initAddr);
|
|
DEBUG_LOG(ME, "video: bufferAddr = %08x, *buffer = %08x, *init = %08x", bufferAddr, buffer, init);
|
|
|
|
// check and decode pmp video
|
|
bool ispmp = false;
|
|
if (decodePmpVideo(ringbuffer, mpeg)){
|
|
DEBUG_LOG(ME, "Using ffmpeg to decode pmp video");
|
|
ispmp = true;
|
|
}
|
|
|
|
if (ringbuffer->packetsRead == 0 || ctx->mediaengine->IsVideoEnd()) {
|
|
WARN_LOG(ME, "sceMpegAvcDecode(%08x, %08x, %d, %08x, %08x): mpeg buffer empty", mpeg, auAddr, frameWidth, bufferAddr, initAddr);
|
|
return hleDelayResult(ERROR_MPEG_AVC_DECODE_FATAL, "mpeg buffer empty", avcEmptyDelayMs);
|
|
}
|
|
|
|
// We stored the video stream id here in sceMpegGetAvcAu().
|
|
ctx->mediaengine->setVideoStream(avcAu.esBuffer);
|
|
|
|
if (ispmp){
|
|
while (pmp_queue.size() != 0){
|
|
// playing all pmp_queue frames
|
|
ctx->mediaengine->m_pFrameRGB = pmp_queue.front();
|
|
int bufferSize = ctx->mediaengine->writeVideoImage(buffer, frameWidth, ctx->videoPixelMode);
|
|
gpu->InvalidateCache(buffer, bufferSize, GPU_INVALIDATE_SAFE);
|
|
ctx->avc.avcFrameStatus = 1;
|
|
ctx->videoFrameCount++;
|
|
|
|
// free front frame
|
|
hleDelayResult(0, "pmp video decode", 30);
|
|
pmp_queue.pop_front();
|
|
}
|
|
}
|
|
else if(ctx->mediaengine->stepVideo(ctx->videoPixelMode)) {
|
|
int bufferSize = ctx->mediaengine->writeVideoImage(buffer, frameWidth, ctx->videoPixelMode);
|
|
gpu->InvalidateCache(buffer, bufferSize, GPU_INVALIDATE_SAFE);
|
|
ctx->avc.avcFrameStatus = 1;
|
|
ctx->videoFrameCount++;
|
|
} else {
|
|
ctx->avc.avcFrameStatus = 0;
|
|
}
|
|
ringbuffer->packetsAvail = ringbuffer->packets - ctx->mediaengine->getRemainSize() / 2048;
|
|
|
|
avcAu.pts = ctx->mediaengine->getVideoTimeStamp() + ctx->mpegFirstTimestamp;
|
|
|
|
// Flush structs back to memory
|
|
avcAu.write(auAddr);
|
|
|
|
// Save the current frame's status to initAddr
|
|
Memory::Write_U32(ctx->avc.avcFrameStatus, initAddr);
|
|
ctx->avc.avcDecodeResult = MPEG_AVC_DECODE_SUCCESS;
|
|
|
|
DEBUG_LOG(ME, "sceMpegAvcDecode(%08x, %08x, %i, %08x, %08x)", mpeg, auAddr, frameWidth, bufferAddr, initAddr);
|
|
|
|
if (ctx->videoFrameCount <= 1)
|
|
return hleDelayResult(0, "mpeg decode", avcFirstDelayMs);
|
|
else
|
|
return hleDelayResult(0, "mpeg decode", avcDecodeDelayMs);
|
|
//hleEatMicro(3300);
|
|
//return hleDelayResult(0, "mpeg decode", 200);
|
|
}
|
|
|
|
static u32 sceMpegAvcDecodeStop(u32 mpeg, u32 frameWidth, u32 bufferAddr, u32 statusAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(bufferAddr) || !Memory::IsValidAddress(statusAddr)){
|
|
ERROR_LOG(ME, "sceMpegAvcDecodeStop(%08x, %08x, %08x, %08x): invalid addresses", mpeg, frameWidth, bufferAddr, statusAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegAvcDecodeStop(%08x, %08x, %08x, %08x): bad mpeg handle", mpeg, frameWidth, bufferAddr, statusAddr);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegAvcDecodeStop(%08x, %08x, %08x, %08x)", mpeg, frameWidth, bufferAddr, statusAddr);
|
|
|
|
// No last frame generated
|
|
Memory::Write_U32(0, statusAddr);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegUnRegistStream(u32 mpeg, int streamUid)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegUnRegistStream(%08x, %i): bad mpeg handle", mpeg, streamUid);
|
|
return -1;
|
|
}
|
|
|
|
StreamInfo info = {0};
|
|
|
|
switch (info.type) {
|
|
case MPEG_AVC_STREAM:
|
|
ctx->avcRegistered = false;
|
|
break;
|
|
case MPEG_AUDIO_STREAM:
|
|
case MPEG_ATRAC_STREAM:
|
|
ctx->atracRegistered = false;
|
|
break;
|
|
case MPEG_PCM_STREAM:
|
|
ctx->pcmRegistered = false;
|
|
break;
|
|
case MPEG_DATA_STREAM:
|
|
ctx->dataRegistered = false;
|
|
break;
|
|
default :
|
|
DEBUG_LOG(ME, "sceMpegUnRegistStream(%i) : unknown streamID ", streamUid);
|
|
break;
|
|
}
|
|
ctx->streamMap[streamUid] = info;
|
|
info.type = -1;
|
|
info.sid = -1 ;
|
|
info.needsReset = true;
|
|
ctx->isAnalyzed = false;
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegAvcDecodeDetail(u32 mpeg, u32 detailAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(detailAddr)){
|
|
WARN_LOG(ME, "sceMpegAvcDecodeDetail(%08x, %08x): invalid addresses", mpeg, detailAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegAvcDecodeDetail(%08x, %08x): bad mpeg handle", mpeg, detailAddr);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegAvcDecodeDetail(%08x, %08x)", mpeg, detailAddr);
|
|
|
|
Memory::Write_U32(ctx->avc.avcDecodeResult, detailAddr + 0);
|
|
Memory::Write_U32(ctx->videoFrameCount, detailAddr + 4);
|
|
Memory::Write_U32(ctx->avc.avcDetailFrameWidth, detailAddr + 8);
|
|
Memory::Write_U32(ctx->avc.avcDetailFrameHeight, detailAddr + 12);
|
|
Memory::Write_U32(0, detailAddr + 16);
|
|
Memory::Write_U32(0, detailAddr + 20);
|
|
Memory::Write_U32(0, detailAddr + 24);
|
|
Memory::Write_U32(0, detailAddr + 28);
|
|
Memory::Write_U32(ctx->avc.avcFrameStatus, detailAddr + 32);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcDecodeStopYCbCr(u32 mpeg, u32 bufferAddr, u32 statusAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(bufferAddr) || !Memory::IsValidAddress(statusAddr)) {
|
|
ERROR_LOG(ME, "UNIMPL sceMpegAvcDecodeStopYCbCr(%08x, %08x, %08x): invalid addresses", mpeg, bufferAddr, statusAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegAvcDecodeStopYCbCr(%08x, %08x, %08x): bad mpeg handle", mpeg, bufferAddr, statusAddr);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG(ME, "UNIMPL sceMpegAvcDecodeStopYCbCr(%08x, %08x, %08x)", mpeg, bufferAddr, statusAddr);
|
|
Memory::Write_U32(0, statusAddr);
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegAvcDecodeYCbCr(u32 mpeg, u32 auAddr, u32 bufferAddr, u32 initAddr)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegAvcDecodeYCbCr(%08x, %08x, %08x, %08x): bad mpeg handle", mpeg, auAddr, bufferAddr, initAddr);
|
|
return -1;
|
|
}
|
|
|
|
SceMpegAu avcAu;
|
|
avcAu.read(auAddr);
|
|
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ctx->mpegRingbufferAddr);
|
|
if (!ringbuffer.IsValid()) {
|
|
ERROR_LOG(ME, "Bogus mpegringbufferaddr");
|
|
return -1;
|
|
}
|
|
|
|
if (ringbuffer->packetsRead == 0 || ctx->mediaengine->IsVideoEnd()) {
|
|
WARN_LOG(ME, "sceMpegAvcDecodeYCbCr(%08x, %08x, %08x, %08x): mpeg buffer empty", mpeg, auAddr, bufferAddr, initAddr);
|
|
return hleDelayResult(ERROR_MPEG_AVC_DECODE_FATAL, "mpeg buffer empty", avcEmptyDelayMs);
|
|
}
|
|
|
|
// We stored the video stream id here in sceMpegGetAvcAu().
|
|
ctx->mediaengine->setVideoStream(avcAu.esBuffer);
|
|
|
|
u32 buffer = Memory::Read_U32(bufferAddr);
|
|
u32 init = Memory::Read_U32(initAddr);
|
|
DEBUG_LOG(ME, "*buffer = %08x, *init = %08x", buffer, init);
|
|
|
|
if (ctx->mediaengine->stepVideo(ctx->videoPixelMode)) {
|
|
// Don't draw here, we'll draw in the Csc func.
|
|
ctx->avc.avcFrameStatus = 1;
|
|
ctx->videoFrameCount++;
|
|
}else {
|
|
ctx->avc.avcFrameStatus = 0;
|
|
}
|
|
ringbuffer->packetsAvail = ringbuffer->packets - ctx->mediaengine->getRemainSize() / 2048;
|
|
|
|
avcAu.pts = ctx->mediaengine->getVideoTimeStamp() + ctx->mpegFirstTimestamp;
|
|
|
|
// Flush structs back to memory
|
|
avcAu.write(auAddr);
|
|
|
|
// Save the current frame's status to initAddr
|
|
Memory::Write_U32(ctx->avc.avcFrameStatus, initAddr);
|
|
ctx->avc.avcDecodeResult = MPEG_AVC_DECODE_SUCCESS;
|
|
|
|
DEBUG_LOG(ME, "sceMpegAvcDecodeYCbCr(%08x, %08x, %08x, %08x)", mpeg, auAddr, bufferAddr, initAddr);
|
|
|
|
if (ctx->videoFrameCount <= 1)
|
|
return hleDelayResult(0, "mpeg decode", avcFirstDelayMs);
|
|
else
|
|
return hleDelayResult(0, "mpeg decode", avcDecodeDelayMs);
|
|
//hleEatMicro(3300);
|
|
//return hleDelayResult(0, "mpeg decode", 200);
|
|
}
|
|
|
|
static u32 sceMpegAvcDecodeFlush(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegAvcDecodeFlush(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG(ME, "UNIMPL sceMpegAvcDecodeFlush(%08x)", mpeg);
|
|
if ( ctx->videoFrameCount > 0 || ctx->audioFrameCount > 0) {
|
|
//__MpegFinish();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegInitAu(u32 mpeg, u32 bufferAddr, u32 auPointer)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegInitAu(%08x, %i, %08x): bad mpeg handle", mpeg, bufferAddr, auPointer);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegInitAu(%08x, %i, %08x)", mpeg, bufferAddr, auPointer);
|
|
|
|
SceMpegAu sceAu;
|
|
sceAu.read(auPointer);
|
|
|
|
if (bufferAddr >= 1 && bufferAddr <= (u32)MPEG_DATA_ES_BUFFERS && ctx->esBuffers[bufferAddr - 1]) {
|
|
// This esbuffer has been allocated for Avc.
|
|
sceAu.esBuffer = bufferAddr; // Can this be right??? not much of a buffer pointer..
|
|
sceAu.esSize = MPEG_AVC_ES_SIZE;
|
|
sceAu.dts = 0;
|
|
sceAu.pts = 0;
|
|
|
|
sceAu.write(auPointer);
|
|
} else {
|
|
// This esbuffer has been left as Atrac.
|
|
sceAu.esBuffer = bufferAddr;
|
|
sceAu.esSize = MPEG_ATRAC_ES_SIZE;
|
|
sceAu.pts = 0;
|
|
sceAu.dts = UNKNOWN_TIMESTAMP;
|
|
|
|
sceAu.write(auPointer);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegQueryAtracEsSize(u32 mpeg, u32 esSizeAddr, u32 outSizeAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(esSizeAddr) || !Memory::IsValidAddress(outSizeAddr)) {
|
|
ERROR_LOG(ME, "sceMpegQueryAtracEsSize(%08x, %08x, %08x): invalid addresses", mpeg, esSizeAddr, outSizeAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegQueryAtracEsSize(%08x, %08x, %08x): bad mpeg handle", mpeg, esSizeAddr, outSizeAddr);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegQueryAtracEsSize(%08x, %08x, %08x)", mpeg, esSizeAddr, outSizeAddr);
|
|
|
|
Memory::Write_U32(MPEG_ATRAC_ES_SIZE, esSizeAddr);
|
|
Memory::Write_U32(MPEG_ATRAC_ES_OUTPUT_SIZE, outSizeAddr);
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegRingbufferAvailableSize(u32 ringbufferAddr)
|
|
{
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ringbufferAddr);
|
|
|
|
if (!ringbuffer.IsValid()) {
|
|
ERROR_LOG(ME, "sceMpegRingbufferAvailableSize(%08x): invalid ringbuffer, should crash", ringbufferAddr);
|
|
return SCE_KERNEL_ERROR_ILLEGAL_ADDRESS;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(ringbuffer->mpeg);
|
|
if (!ctx) {
|
|
ERROR_LOG(ME, "sceMpegRingbufferAvailableSize(%08x): bad mpeg handle", ringbufferAddr);
|
|
return ERROR_MPEG_NOT_YET_INIT;
|
|
}
|
|
|
|
ctx->mpegRingbufferAddr = ringbufferAddr;
|
|
hleEatCycles(2020);
|
|
hleReSchedule("mpeg ringbuffer avail");
|
|
|
|
static int lastAvail = 0;
|
|
if (lastAvail != ringbuffer->packetsAvail) {
|
|
DEBUG_LOG(ME, "%i=sceMpegRingbufferAvailableSize(%08x)", ringbuffer->packets - ringbuffer->packetsAvail, ringbufferAddr);
|
|
lastAvail = ringbuffer->packetsAvail;
|
|
} else {
|
|
VERBOSE_LOG(ME, "%i=sceMpegRingbufferAvailableSize(%08x)", ringbuffer->packets - ringbuffer->packetsAvail, ringbufferAddr);
|
|
}
|
|
return ringbuffer->packets - ringbuffer->packetsAvail;
|
|
}
|
|
|
|
void PostPutAction::run(MipsCall &call) {
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ringAddr_);
|
|
|
|
MpegContext *ctx = getMpegCtx(ringbuffer->mpeg);
|
|
|
|
int packetsAdded = currentMIPS->r[MIPS_REG_V0];
|
|
if (ringbuffer->packetsRead == 0 && ctx->mediaengine && packetsAdded > 0) {
|
|
// init mediaEngine
|
|
AnalyzeMpeg(ctx->mpegheader, ctx);
|
|
ctx->mediaengine->loadStream(ctx->mpegheader, 2048, ringbuffer->packets * ringbuffer->packetSize);
|
|
}
|
|
if (packetsAdded > 0) {
|
|
if (packetsAdded > ringbuffer->packets - ringbuffer->packetsAvail) {
|
|
WARN_LOG(ME, "sceMpegRingbufferPut clamping packetsAdded old=%i new=%i", packetsAdded, ringbuffer->packets - ringbuffer->packetsAvail);
|
|
packetsAdded = ringbuffer->packets - ringbuffer->packetsAvail;
|
|
}
|
|
int actuallyAdded = ctx->mediaengine == NULL ? 8 : ctx->mediaengine->addStreamData(Memory::GetPointer(ringbuffer->data), packetsAdded * 2048) / 2048;
|
|
if (actuallyAdded != packetsAdded) {
|
|
WARN_LOG_REPORT(ME, "sceMpegRingbufferPut(): unable to enqueue all added packets, going to overwrite some frames.");
|
|
}
|
|
ringbuffer->packetsRead += packetsAdded;
|
|
ringbuffer->packetsWritten += packetsAdded;
|
|
ringbuffer->packetsAvail += packetsAdded;
|
|
}
|
|
DEBUG_LOG(ME, "packetAdded: %i packetsRead: %i packetsTotal: %i", packetsAdded, ringbuffer->packetsRead, ringbuffer->packets);
|
|
|
|
call.setReturnValue(packetsAdded);
|
|
}
|
|
|
|
|
|
// Program signals that it has written data to the ringbuffer and gets a callback ?
|
|
static u32 sceMpegRingbufferPut(u32 ringbufferAddr, u32 numPackets, u32 available)
|
|
{
|
|
numPackets = std::min(numPackets, available);
|
|
if (numPackets <= 0) {
|
|
DEBUG_LOG(ME, "sceMpegRingbufferPut(%08x, %i, %i): no packets to enqueue", ringbufferAddr, numPackets, available);
|
|
return 0;
|
|
}
|
|
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ringbufferAddr);
|
|
if (!ringbuffer.IsValid()) {
|
|
// Would have crashed before, TODO test behavior.
|
|
ERROR_LOG_REPORT(ME, "sceMpegRingbufferPut(%08x, %i, %i): invalid ringbuffer address", ringbufferAddr, numPackets, available);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(ringbuffer->mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegRingbufferPut(%08x, %i, %i): bad mpeg handle %08x", ringbufferAddr, numPackets, available, ringbuffer->mpeg);
|
|
return -1;
|
|
}
|
|
|
|
// Execute callback function as a direct MipsCall, no blocking here so no messing around with wait states etc
|
|
if (ringbuffer->callback_addr != 0) {
|
|
DEBUG_LOG(ME, "sceMpegRingbufferPut(%08x, %i, %i)", ringbufferAddr, numPackets, available);
|
|
|
|
PostPutAction *action = (PostPutAction *)__KernelCreateAction(actionPostPut);
|
|
action->setRingAddr(ringbufferAddr);
|
|
// TODO: Should call this multiple times until we get numPackets.
|
|
// Normally this would be if it did not read enough, but also if available > packets.
|
|
// Should ultimately return the TOTAL number of returned packets.
|
|
u32 packetsThisRound = std::min(numPackets, (u32)ringbuffer->packets);
|
|
u32 args[3] = {(u32)ringbuffer->data, packetsThisRound, (u32)ringbuffer->callback_args};
|
|
__KernelDirectMipsCall(ringbuffer->callback_addr, action, args, 3, false);
|
|
} else {
|
|
ERROR_LOG_REPORT(ME, "sceMpegRingbufferPut: callback_addr zero");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegGetAvcAu(u32 mpeg, u32 streamId, u32 auAddr, u32 attrAddr)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegGetAvcAu(%08x, %08x, %08x, %08x): bad mpeg handle", mpeg, streamId, auAddr, attrAddr);
|
|
return -1;
|
|
}
|
|
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ctx->mpegRingbufferAddr);
|
|
if (!ringbuffer.IsValid()) {
|
|
// Would have crashed before, TODO test behavior.
|
|
ERROR_LOG_REPORT(ME, "sceMpegGetAvcAu(%08x, %08x, %08x, %08x): invalid ringbuffer address", mpeg, streamId, auAddr, attrAddr);
|
|
return -1;
|
|
}
|
|
|
|
SceMpegAu avcAu;
|
|
avcAu.read(auAddr);
|
|
|
|
if (ringbuffer->packetsRead == 0 || ringbuffer->packetsAvail == 0) {
|
|
DEBUG_LOG(ME, "ERROR_MPEG_NO_DATA=sceMpegGetAvcAu(%08x, %08x, %08x, %08x)", mpeg, streamId, auAddr, attrAddr);
|
|
avcAu.pts = -1;
|
|
avcAu.dts = -1;
|
|
avcAu.write(auAddr);
|
|
// TODO: Does this really reschedule?
|
|
return hleDelayResult(ERROR_MPEG_NO_DATA, "mpeg get avc", mpegDecodeErrorDelayMs);
|
|
}
|
|
|
|
auto streamInfo = ctx->streamMap.find(streamId);
|
|
if (streamInfo == ctx->streamMap.end()) {
|
|
WARN_LOG_REPORT(ME, "sceMpegGetAvcAu: invalid video stream %08x", streamId);
|
|
return -1;
|
|
}
|
|
|
|
if (streamInfo->second.needsReset) {
|
|
avcAu.pts = 0;
|
|
streamInfo->second.needsReset = false;
|
|
}
|
|
|
|
// esBuffer is the memory where this au data goes. We don't write the data to memory.
|
|
// Instead, let's abuse it to keep track of the stream number.
|
|
avcAu.esBuffer = streamInfo->second.num;
|
|
|
|
/*// Wait for audio if too much ahead
|
|
if (ctx->atracRegistered && (ctx->mediaengine->getVideoTimeStamp() > ctx->mediaengine->getAudioTimeStamp() + getMaxAheadTimestamp(mpegRingbuffer)))
|
|
{
|
|
ERROR_LOG(ME, "sceMpegGetAvcAu - video too much ahead");
|
|
// TODO: Does this really reschedule?
|
|
return hleDelayResult(ERROR_MPEG_NO_DATA, "mpeg get avc", mpegDecodeErrorDelayMs);
|
|
}*/
|
|
|
|
int result = 0;
|
|
|
|
avcAu.pts = ctx->mediaengine->getVideoTimeStamp() + ctx->mpegFirstTimestamp;
|
|
avcAu.dts = avcAu.pts - videoTimestampStep;
|
|
|
|
if (ctx->mediaengine->IsVideoEnd()) {
|
|
INFO_LOG(ME, "video end reach. pts: %i dts: %i", (int)avcAu.pts, (int)ctx->mediaengine->getLastTimeStamp());
|
|
ringbuffer->packetsAvail = 0;
|
|
|
|
result = ERROR_MPEG_NO_DATA;
|
|
}
|
|
|
|
// The avcau struct may have been modified by mediaengine, write it back.
|
|
avcAu.write(auAddr);
|
|
|
|
// Jeanne d'Arc return 00000000 as attrAddr here and cause WriteToHardware error
|
|
if (Memory::IsValidAddress(attrAddr)) {
|
|
Memory::Write_U32(1, attrAddr);
|
|
}
|
|
|
|
|
|
DEBUG_LOG(ME, "%x=sceMpegGetAvcAu(%08x, %08x, %08x, %08x)", result, mpeg, streamId, auAddr, attrAddr);
|
|
// TODO: sceMpegGetAvcAu seems to modify esSize, and delay when it's > 1000 or something.
|
|
// There's definitely more to it, but ultimately it seems games should expect it to delay randomly.
|
|
return hleDelayResult(result, "mpeg get avc", 100);
|
|
}
|
|
|
|
static u32 sceMpegFinish()
|
|
{
|
|
if (!isMpegInit) {
|
|
WARN_LOG(ME, "sceMpegFinish(...): not initialized");
|
|
// TODO: Need to properly hook module load/unload for this to work right.
|
|
//return ERROR_MPEG_NOT_YET_INIT;
|
|
} else {
|
|
INFO_LOG(ME, "sceMpegFinish(...)");
|
|
__VideoPmpShutdown();
|
|
}
|
|
isMpegInit = false;
|
|
//__MpegFinish();
|
|
return hleDelayResult(0, "mpeg finish", 250);
|
|
}
|
|
|
|
static u32 sceMpegQueryMemSize()
|
|
{
|
|
DEBUG_LOG(ME, "%i = sceMpegQueryMemSize()",MPEG_MEMSIZE);
|
|
return MPEG_MEMSIZE;
|
|
}
|
|
|
|
static int sceMpegGetAtracAu(u32 mpeg, u32 streamId, u32 auAddr, u32 attrAddr)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegGetAtracAu(%08x, %08x, %08x, %08x): bad mpeg handle", mpeg, streamId, auAddr, attrAddr);
|
|
return -1;
|
|
}
|
|
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ctx->mpegRingbufferAddr);
|
|
if (!ringbuffer.IsValid()) {
|
|
// Would have crashed before, TODO test behavior.
|
|
WARN_LOG(ME, "sceMpegGetAtracAu(%08x, %08x, %08x, %08x): invalid ringbuffer address", mpeg, streamId, auAddr, attrAddr);
|
|
return -1;
|
|
}
|
|
|
|
SceMpegAu atracAu;
|
|
atracAu.read(auAddr);
|
|
|
|
auto streamInfo = ctx->streamMap.find(streamId);
|
|
if (streamInfo != ctx->streamMap.end() && streamInfo->second.needsReset) {
|
|
atracAu.pts = 0;
|
|
streamInfo->second.needsReset = false;
|
|
}
|
|
if (streamInfo == ctx->streamMap.end()) {
|
|
WARN_LOG_REPORT(ME, "sceMpegGetAtracAu: invalid audio stream %08x", streamId);
|
|
// TODO: Why was this changed to not return an error?
|
|
}
|
|
|
|
// The audio can end earlier than the video does.
|
|
if (ringbuffer->packetsAvail == 0) {
|
|
DEBUG_LOG(ME, "ERROR_MPEG_NO_DATA=sceMpegGetAtracAu(%08x, %08x, %08x, %08x)", mpeg, streamId, auAddr, attrAddr);
|
|
// TODO: Does this really delay?
|
|
return hleDelayResult(ERROR_MPEG_NO_DATA, "mpeg get atrac", mpegDecodeErrorDelayMs);
|
|
}
|
|
|
|
// esBuffer is the memory where this au data goes. We don't write the data to memory.
|
|
// Instead, let's abuse it to keep track of the stream number.
|
|
if (streamInfo != ctx->streamMap.end()) {
|
|
atracAu.esBuffer = streamInfo->second.num;
|
|
}
|
|
|
|
int result = 0;
|
|
atracAu.pts = ctx->mediaengine->getAudioTimeStamp() + ctx->mpegFirstTimestamp;
|
|
|
|
if (ctx->mediaengine->IsVideoEnd()) {
|
|
INFO_LOG(ME, "video end reach. pts: %i dts: %i", (int)atracAu.pts, (int)ctx->mediaengine->getLastTimeStamp());
|
|
ringbuffer->packetsAvail = 0;
|
|
// TODO: Is this correct?
|
|
if (!ctx->mediaengine->IsNoAudioData()) {
|
|
WARN_LOG_REPORT(ME, "Video end without audio end, potentially skipping some audio?");
|
|
}
|
|
result = ERROR_MPEG_NO_DATA;
|
|
}
|
|
|
|
if (ctx->atracRegistered && ctx->mediaengine->IsNoAudioData() && !ctx->endOfAudioReached) {
|
|
WARN_LOG(ME, "Audio end reach. pts: %i dts: %i", (int)atracAu.pts, (int)ctx->mediaengine->getLastTimeStamp());
|
|
ctx->endOfAudioReached = true;
|
|
}
|
|
if (ctx->mediaengine->IsNoAudioData()) {
|
|
result = ERROR_MPEG_NO_DATA;
|
|
}
|
|
|
|
atracAu.write(auAddr);
|
|
|
|
// 3rd birthday return 00000000 as attrAddr here and cause WriteToHardware error
|
|
if (Memory::IsValidAddress(attrAddr)) {
|
|
Memory::Write_U32(0, attrAddr);
|
|
}
|
|
|
|
DEBUG_LOG(ME, "%x=sceMpegGetAtracAu(%08x, %08x, %08x, %08x)", result, mpeg, streamId, auAddr, attrAddr);
|
|
// TODO: Not clear on exactly when this delays.
|
|
return hleDelayResult(result, "mpeg get atrac", 100);
|
|
}
|
|
|
|
static int sceMpegQueryPcmEsSize(u32 mpeg, u32 esSizeAddr, u32 outSizeAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(esSizeAddr) || !Memory::IsValidAddress(outSizeAddr)) {
|
|
ERROR_LOG(ME, "sceMpegQueryPcmEsSize(%08x, %08x, %08x): invalid addresses", mpeg, esSizeAddr, outSizeAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegQueryPcmEsSize(%08x, %08x, %08x): bad mpeg handle", mpeg, esSizeAddr, outSizeAddr);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG(ME, "sceMpegQueryPcmEsSize(%08x, %08x, %08x)", mpeg, esSizeAddr, outSizeAddr);
|
|
|
|
Memory::Write_U32(MPEG_PCM_ES_SIZE, esSizeAddr);
|
|
Memory::Write_U32(MPEG_PCM_ES_OUTPUT_SIZE, outSizeAddr);
|
|
return 0;
|
|
}
|
|
|
|
|
|
static u32 sceMpegChangeGetAuMode(u32 mpeg, int streamUid, int mode)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegChangeGetAuMode(%08x, %i, %i): bad mpeg handle", mpeg, streamUid, mode);
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
}
|
|
if (mode != MPEG_AU_MODE_DECODE && mode != MPEG_AU_MODE_SKIP) {
|
|
ERROR_LOG(ME, "UNIMPL sceMpegChangeGetAuMode(%08x, %i, %i): bad mode", mpeg, streamUid, mode);
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
}
|
|
|
|
auto stream = ctx->streamMap.find(streamUid);
|
|
if (stream == ctx->streamMap.end()) {
|
|
ERROR_LOG(ME, "UNIMPL sceMpegChangeGetAuMode(%08x, %i, %i): unknown streamID", mpeg, streamUid, mode);
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
} else {
|
|
StreamInfo &info = stream->second;
|
|
DEBUG_LOG(ME, "UNIMPL sceMpegChangeGetAuMode(%08x, %i, %i): changing type=%d", mpeg, streamUid, mode, info.type);
|
|
switch (info.type) {
|
|
case MPEG_AVC_STREAM:
|
|
if (mode == MPEG_AU_MODE_DECODE) {
|
|
ctx->ignoreAvc = false;
|
|
} else if (mode == MPEG_AU_MODE_SKIP) {
|
|
ctx->ignoreAvc = true;
|
|
}
|
|
break;
|
|
case MPEG_AUDIO_STREAM:
|
|
case MPEG_ATRAC_STREAM:
|
|
if (mode == MPEG_AU_MODE_DECODE) {
|
|
ctx->ignoreAtrac = false;
|
|
} else if (mode == MPEG_AU_MODE_SKIP) {
|
|
ctx->ignoreAtrac = true;
|
|
}
|
|
break;
|
|
case MPEG_PCM_STREAM:
|
|
if (mode == MPEG_AU_MODE_DECODE) {
|
|
ctx->ignorePcm = false;
|
|
} else if (mode == MPEG_AU_MODE_SKIP) {
|
|
ctx->ignorePcm = true;
|
|
}
|
|
break;
|
|
default:
|
|
ERROR_LOG(ME, "UNIMPL sceMpegChangeGetAuMode(%08x, %i, %i): unknown streamID", mpeg, streamUid, mode);
|
|
break;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegChangeGetAvcAuMode(u32 mpeg, u32 stream_addr, int mode)
|
|
{
|
|
if (!Memory::IsValidAddress(stream_addr)) {
|
|
ERROR_LOG(ME, "UNIMPL sceMpegChangeGetAvcAuMode(%08x, %08x, %i): invalid addresses", mpeg, stream_addr, mode);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegChangeGetAvcAuMode(%08x, %08x, %i): bad mpeg handle", mpeg, stream_addr, mode);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG_REPORT_ONCE(mpegChangeAvcAu, ME, "UNIMPL sceMpegChangeGetAvcAuMode(%08x, %08x, %i)", mpeg, stream_addr, mode);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegGetPcmAu(u32 mpeg, int streamUid, u32 auAddr, u32 attrAddr)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegGetPcmAu(%08x, %i, %08x, %08x): bad mpeg handle", mpeg, streamUid, auAddr, attrAddr);
|
|
return -1;
|
|
}
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ctx->mpegRingbufferAddr);
|
|
if (!ringbuffer.IsValid()) {
|
|
// Would have crashed before, TODO test behavior
|
|
WARN_LOG(ME, "sceMpegGetPcmAu(%08x, %08x, %08x, %08x): invalid ringbuffer address", mpeg, streamUid, auAddr, attrAddr);
|
|
return -1;
|
|
}
|
|
if (!Memory::IsValidAddress(streamUid)) {
|
|
WARN_LOG(ME, "sceMpegGetPcmAu(%08x, %08x, %08x, %08x): didn't get a fake stream", mpeg, streamUid, auAddr, attrAddr);
|
|
return ERROR_MPEG_INVALID_ADDR;
|
|
}
|
|
SceMpegAu atracAu;
|
|
atracAu.read(auAddr);
|
|
auto streamInfo = ctx->streamMap.find(streamUid);
|
|
if (streamInfo == ctx->streamMap.end()) {
|
|
WARN_LOG(ME, "sceMpegGetPcmAu(%08x, %08x, %08x, %08x): bad streamUid ", mpeg, streamUid, auAddr, attrAddr);
|
|
return -1;
|
|
}
|
|
|
|
atracAu.write(auAddr);
|
|
u32 attr = 1 << 7; // Sampling rate (1 = 44.1kHz).
|
|
attr |= 2; // Number of channels (1 - MONO / 2 - STEREO).
|
|
if (Memory::IsValidAddress(attrAddr))
|
|
Memory::Write_U32(attr, attrAddr);
|
|
|
|
ERROR_LOG_REPORT_ONCE(mpegPcmAu, ME, "UNIMPL sceMpegGetPcmAu(%08x, %i, %08x, %08x)", mpeg, streamUid, auAddr, attrAddr);
|
|
return 0;
|
|
}
|
|
|
|
static int __MpegRingbufferQueryPackNum(u32 memorySize) {
|
|
return memorySize / (2048 + 104);
|
|
}
|
|
|
|
static int sceMpegRingbufferQueryPackNum(u32 memorySize) {
|
|
DEBUG_LOG(ME, "sceMpegRingbufferQueryPackNum(%i)", memorySize);
|
|
return __MpegRingbufferQueryPackNum(memorySize);
|
|
}
|
|
|
|
static u32 sceMpegFlushAllStream(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegFlushAllStream(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
WARN_LOG(ME, "UNIMPL sceMpegFlushAllStream(%08x)", mpeg);
|
|
|
|
ctx->isAnalyzed = false;
|
|
|
|
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ctx->mpegRingbufferAddr);
|
|
if (ringbuffer.IsValid()) {
|
|
ringbuffer->packetsAvail = 0;
|
|
ringbuffer->packetsRead = 0;
|
|
ringbuffer->packetsWritten = 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegFlushStream(u32 mpeg, int stream_addr)
|
|
{
|
|
if (!Memory::IsValidAddress(stream_addr)) {
|
|
ERROR_LOG(ME, "UNIMPL sceMpegFlushStream(%08x, %i): invalid addresses", mpeg , stream_addr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegFlushStream(%08x, %i): bad mpeg handle", mpeg , stream_addr);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG(ME, "UNIMPL sceMpegFlushStream(%08x, %i)", mpeg , stream_addr);
|
|
//__MpegFinish();
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcCopyYCbCr(u32 mpeg, u32 sourceAddr, u32 YCbCrAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(sourceAddr) || !Memory::IsValidAddress(YCbCrAddr)) {
|
|
ERROR_LOG(ME, "UNIMPL sceMpegAvcCopyYCbCr(%08x, %08x, %08x): invalid addresses", mpeg, sourceAddr, YCbCrAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegAvcCopyYCbCr(%08x, %08x, %08x): bad mpeg handle", mpeg, sourceAddr, YCbCrAddr);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG(ME, "UNIMPL sceMpegAvcCopyYCbCr(%08x, %08x, %08x)", mpeg, sourceAddr, YCbCrAddr);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAtracDecode(u32 mpeg, u32 auAddr, u32 bufferAddr, int init)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegAtracDecode(%08x, %08x, %08x, %i): bad mpeg handle", mpeg, auAddr, bufferAddr, init);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegAtracDecode(%08x, %08x, %08x, %i)", mpeg, auAddr, bufferAddr, init);
|
|
|
|
SceMpegAu atracAu;
|
|
atracAu.read(auAddr);
|
|
|
|
// We kept track of the stream number here in sceMpegGetAtracAu().
|
|
ctx->mediaengine->setAudioStream(atracAu.esBuffer);
|
|
|
|
Memory::Memset(bufferAddr, 0, MPEG_ATRAC_ES_OUTPUT_SIZE);
|
|
ctx->mediaengine->getAudioSamples(bufferAddr);
|
|
atracAu.pts = ctx->mediaengine->getAudioTimeStamp() + ctx->mpegFirstTimestamp;
|
|
|
|
atracAu.write(auAddr);
|
|
|
|
|
|
return hleDelayResult(0, "mpeg atrac decode", atracDecodeDelayMs);
|
|
//hleEatMicro(4000);
|
|
//return hleDelayResult(0, "mpeg atrac decode", 200);
|
|
}
|
|
|
|
// YCbCr -> RGB color space conversion
|
|
static u32 sceMpegAvcCsc(u32 mpeg, u32 sourceAddr, u32 rangeAddr, int frameWidth, u32 destAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(sourceAddr) || !Memory::IsValidAddress(rangeAddr) || !Memory::IsValidAddress(destAddr)) {
|
|
ERROR_LOG(ME, "sceMpegAvcCsc(%08x, %08x, %08x, %i, %08x): invalid addresses", mpeg, sourceAddr, rangeAddr, frameWidth, destAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegAvcCsc(%08x, %08x, %08x, %i, %08x): bad mpeg handle", mpeg, sourceAddr, rangeAddr, frameWidth, destAddr);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegAvcCsc(%08x, %08x, %08x, %i, %08x)", mpeg, sourceAddr, rangeAddr, frameWidth, destAddr);
|
|
|
|
int x = Memory::Read_U32(rangeAddr);
|
|
int y = Memory::Read_U32(rangeAddr + 4);
|
|
int width = Memory::Read_U32(rangeAddr + 8);
|
|
int height = Memory::Read_U32(rangeAddr + 12);
|
|
int destSize = ctx->mediaengine->writeVideoImageWithRange(destAddr, frameWidth, ctx->videoPixelMode, x, y, width, height);
|
|
|
|
gpu->InvalidateCache(destAddr, destSize, GPU_INVALIDATE_SAFE);
|
|
// Do not use avcDecodeDelayMs 's value
|
|
// Will cause video 's screen dislocation in Bleach heat of soul 6
|
|
// https://github.com/hrydgard/ppsspp/issues/5535
|
|
// If do not use DelayResult,Wil cause flickering in Dengeki no Pilot: Tenkuu no Kizuna
|
|
// https://github.com/hrydgard/ppsspp/issues/7549
|
|
|
|
return hleDelayResult(0, "mpeg avc csc", avcCscDelayMs);
|
|
}
|
|
|
|
static u32 sceMpegRingbufferDestruct(u32 ringbufferAddr)
|
|
{
|
|
DEBUG_LOG(ME, "sceMpegRingbufferDestruct(%08x)", ringbufferAddr);
|
|
// Apparently, does nothing.
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcInitYCbCr(u32 mpeg, int mode, int width, int height, u32 ycbcr_addr)
|
|
{
|
|
if (!Memory::IsValidAddress(ycbcr_addr)) {
|
|
ERROR_LOG(ME, "UNIMPL sceMpegAvcInitYCbCr(%08x, %i, %i, %i, %08x): invalid addresses", mpeg, mode, width, height, ycbcr_addr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegAvcInitYCbCr(%08x, %i, %i, %i, %08x): bad mpeg handle", mpeg, mode, width, height, ycbcr_addr);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG(ME, "UNIMPL sceMpegAvcInitYCbCr(%08x, %i, %i, %i, %08x)", mpeg, mode, width, height, ycbcr_addr);
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegAvcQueryYCbCrSize(u32 mpeg, u32 mode, u32 width, u32 height, u32 resultAddr)
|
|
{
|
|
if ((width & 15) != 0 || (height & 15) != 0 || height > 272 || width > 480) {
|
|
ERROR_LOG(ME, "sceMpegAvcQueryYCbCrSize: bad w/h %i x %i", width, height);
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegAvcQueryYCbCrSize(%08x, %i, %i, %i, %08x)", mpeg, mode, width, height, resultAddr);
|
|
|
|
int size = (width / 2) * (height / 2) * 6 + 128;
|
|
Memory::Write_U32(size, resultAddr);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegQueryUserdataEsSize(u32 mpeg, u32 esSizeAddr, u32 outSizeAddr)
|
|
{
|
|
if (!Memory::IsValidAddress(esSizeAddr) || !Memory::IsValidAddress(outSizeAddr)) {
|
|
ERROR_LOG(ME, "sceMpegQueryUserdataEsSize(%08x, %08x, %08x): invalid addresses", mpeg, esSizeAddr, outSizeAddr);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegQueryUserdataEsSize(%08x, %08x, %08x): bad mpeg handle", mpeg, esSizeAddr, outSizeAddr);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegQueryUserdataEsSize(%08x, %08x, %08x)", mpeg, esSizeAddr, outSizeAddr);
|
|
|
|
Memory::Write_U32(MPEG_DATA_ES_SIZE, esSizeAddr);
|
|
Memory::Write_U32(MPEG_DATA_ES_OUTPUT_SIZE, outSizeAddr);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcResourceGetAvcDecTopAddr(u32 mpeg)
|
|
{
|
|
ERROR_LOG(ME, "UNIMPL sceMpegAvcResourceGetAvcDecTopAddr(%08x)", mpeg);
|
|
// it's just a random address
|
|
return 0x12345678;
|
|
}
|
|
|
|
static u32 sceMpegAvcResourceFinish(u32 mpeg)
|
|
{
|
|
DEBUG_LOG(ME,"UNIMPL sceMpegAvcResourceFinish(%08x)", mpeg);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcResourceGetAvcEsBuf(u32 mpeg)
|
|
{
|
|
ERROR_LOG_REPORT_ONCE(mpegResourceEsBuf, ME, "UNIMPL sceMpegAvcResourceGetAvcEsBuf(%08x)", mpeg);
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcResourceInit(u32 mpeg)
|
|
{
|
|
if (mpeg != 1) {
|
|
return ERROR_MPEG_INVALID_VALUE;
|
|
}
|
|
|
|
ERROR_LOG(ME, "UNIMPL sceMpegAvcResourceInit(%08x)", mpeg);
|
|
return 0;
|
|
}
|
|
|
|
static u32 convertABGRToYCbCr(u32 abgr) {
|
|
//see http://en.wikipedia.org/wiki/Yuv#Y.27UV444_to_RGB888_conversion for more information.
|
|
u8 r = (abgr >> 0) & 0xFF;
|
|
u8 g = (abgr >> 8) & 0xFF;
|
|
u8 b = (abgr >> 16) & 0xFF;
|
|
int y = 0.299f * r + 0.587f * g + 0.114f * b + 0;
|
|
int cb = -0.169f * r - 0.331f * g + 0.499f * b + 128.0f;
|
|
int cr = 0.499f * r - 0.418f * g - 0.0813f * b + 128.0f;
|
|
|
|
// check yCbCr value
|
|
if ( y > 0xFF) y = 0xFF; if ( y < 0) y = 0;
|
|
if (cb > 0xFF) cb = 0xFF; if (cb < 0) cb = 0;
|
|
if (cr > 0xFF) cr = 0xFF; if (cr < 0) cr = 0;
|
|
|
|
return (y << 16) | (cb << 8) | cr;
|
|
}
|
|
|
|
static int __MpegAvcConvertToYuv420(const void *data, u32 bufferOutputAddr, int width, int height) {
|
|
u32 *imageBuffer = (u32*)data;
|
|
int sizeY = width * height;
|
|
int sizeCb = sizeY >> 2;
|
|
u8 *Y = (u8*)Memory::GetPointer(bufferOutputAddr);
|
|
u8 *Cb = Y + sizeY;
|
|
u8 *Cr = Cb + sizeCb;
|
|
|
|
for (int y = 0; y < height; ++y) {
|
|
for (int x = 0; x < width; x += 4) {
|
|
u32 abgr0 = imageBuffer[x + 0];
|
|
u32 abgr1 = imageBuffer[x + 1];
|
|
u32 abgr2 = imageBuffer[x + 2];
|
|
u32 abgr3 = imageBuffer[x + 3];
|
|
|
|
u32 yCbCr0 = convertABGRToYCbCr(abgr0);
|
|
u32 yCbCr1 = convertABGRToYCbCr(abgr1);
|
|
u32 yCbCr2 = convertABGRToYCbCr(abgr2);
|
|
u32 yCbCr3 = convertABGRToYCbCr(abgr3);
|
|
|
|
Y[x + 0] = (yCbCr0 >> 16) & 0xFF;
|
|
Y[x + 1] = (yCbCr1 >> 16) & 0xFF;
|
|
Y[x + 2] = (yCbCr2 >> 16) & 0xFF;
|
|
Y[x + 3] = (yCbCr3 >> 16) & 0xFF;
|
|
|
|
*Cb++ = (yCbCr0 >> 8) & 0xFF;
|
|
*Cr++ = yCbCr0 & 0xFF;
|
|
}
|
|
imageBuffer += width;
|
|
Y += width ;
|
|
}
|
|
return (width << 16) | height;
|
|
}
|
|
|
|
static int sceMpegAvcConvertToYuv420(u32 mpeg, u32 bufferOutputAddr, u32 unknown1, int unknown2)
|
|
{
|
|
if (!Memory::IsValidAddress(bufferOutputAddr)) {
|
|
ERROR_LOG(ME, "sceMpegAvcConvertToYuv420(%08x, %08x, %08x, %08x): invalid addresses", mpeg, bufferOutputAddr, unknown1, unknown2);
|
|
return -1;
|
|
}
|
|
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegAvcConvertToYuv420(%08x, %08x, %08x, %08x): bad mpeg handle", mpeg, bufferOutputAddr, unknown1, unknown2);
|
|
return -1;
|
|
}
|
|
|
|
if (ctx->mediaengine->m_buffer == 0){
|
|
WARN_LOG(ME, "sceMpegAvcConvertToYuv420(%08x, %08x, %08x, %08x): m_buffer is zero ", mpeg, bufferOutputAddr, unknown1, unknown2);
|
|
return ERROR_MPEG_AVC_INVALID_VALUE;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegAvcConvertToYuv420(%08x, %08x, %08x, %08x)", mpeg, bufferOutputAddr, unknown1, unknown2);
|
|
const u8 *data = ctx->mediaengine->getFrameImage();
|
|
int width = ctx->mediaengine->m_desWidth;
|
|
int height = ctx->mediaengine->m_desHeight;
|
|
|
|
if (data) {
|
|
__MpegAvcConvertToYuv420(data, bufferOutputAddr, width, height);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int sceMpegGetUserdataAu(u32 mpeg, u32 streamUid, u32 auAddr, u32 resultAddr)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "sceMpegGetUserdataAu(%08x, %08x, %08x, %08x): bad mpeg handle", mpeg, streamUid, auAddr, resultAddr);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegGetUserdataAu(%08x, %08x, %08x, %08x)", mpeg, streamUid, auAddr, resultAddr);
|
|
|
|
// TODO: Are these at all right? Seen in Phantasy Star Portable 2.
|
|
Memory::Write_U32(0, resultAddr);
|
|
Memory::Write_U32(0, resultAddr + 4);
|
|
|
|
// We currently can't demux userdata so this seems like the best thing to return in the meantime..
|
|
// Then we probably shouldn't do the above writes? but it works...
|
|
return ERROR_MPEG_NO_DATA;
|
|
}
|
|
|
|
static u32 sceMpegNextAvcRpAu(u32 mpeg, u32 streamUid)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegNextAvcRpAu(%08x, %08x): bad mpeg handle", mpeg, streamUid);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG_REPORT(ME, "UNIMPL sceMpegNextAvcRpAu(%08x, %08x)", mpeg, streamUid);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegGetAvcNalAu(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegGetAvcNalAu(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG_REPORT(ME, "UNIMPL sceMpegGetAvcNalAu(%08x)", mpeg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcDecodeDetailIndex(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegAvcDecodeDetailIndex(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG_REPORT(ME, "UNIMPL sceMpegAvcDecodeDetailIndex(%08x)", mpeg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcDecodeDetail2(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegAvcDecodeDetail2(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG_REPORT(ME, "UNIMPL sceMpegAvcDecodeDetail2(%08x)", mpeg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegGetAvcEsAu(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegGetAvcEsAu(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG_REPORT(ME, "UNIMPL sceMpegGetAvcEsAu(%08x)", mpeg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcCscInfo(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegAvcCscInfo(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG_REPORT(ME, "UNIMPL sceMpegAvcCscInfo(%08x)", mpeg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegAvcCscMode(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegAvcCscMode(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG_REPORT(ME, "UNIMPL sceMpegAvcCscMode(%08x)", mpeg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u32 sceMpegFlushAu(u32 mpeg)
|
|
{
|
|
MpegContext *ctx = getMpegCtx(mpeg);
|
|
if (!ctx) {
|
|
WARN_LOG(ME, "UNIMPL sceMpegFlushAu(%08x): bad mpeg handle", mpeg);
|
|
return -1;
|
|
}
|
|
|
|
ERROR_LOG_REPORT(ME, "UNIMPL sceMpegFlushAu(%08x)", mpeg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
const HLEFunction sceMpeg[] =
|
|
{
|
|
{0XE1CE83A7, &WrapI_UUUU<sceMpegGetAtracAu>, "sceMpegGetAtracAu", 'i', "xxxx" },
|
|
{0XFE246728, &WrapI_UUUU<sceMpegGetAvcAu>, "sceMpegGetAvcAu", 'i', "xxxx" },
|
|
{0XD8C5F121, &WrapU_UUUUUUU<sceMpegCreate>, "sceMpegCreate", 'x', "xxxxxxx"},
|
|
{0XF8DCB679, &WrapI_UUU<sceMpegQueryAtracEsSize>, "sceMpegQueryAtracEsSize", 'i', "xxx" },
|
|
{0XC132E22F, &WrapU_V<sceMpegQueryMemSize>, "sceMpegQueryMemSize", 'x', "" },
|
|
{0X21FF80E4, &WrapI_UUU<sceMpegQueryStreamOffset>, "sceMpegQueryStreamOffset", 'i', "xxx" },
|
|
{0X611E9E11, &WrapU_UU<sceMpegQueryStreamSize>, "sceMpegQueryStreamSize", 'x', "xx" },
|
|
{0X42560F23, &WrapI_UUU<sceMpegRegistStream>, "sceMpegRegistStream", 'i', "xxx" },
|
|
{0X591A4AA2, &WrapU_UI<sceMpegUnRegistStream>, "sceMpegUnRegistStream", 'x', "xi" },
|
|
{0X707B7629, &WrapU_U<sceMpegFlushAllStream>, "sceMpegFlushAllStream", 'x', "x" },
|
|
{0X500F0429, &WrapU_UI<sceMpegFlushStream>, "sceMpegFlushStream", 'x', "xi" },
|
|
{0XA780CF7E, &WrapI_U<sceMpegMallocAvcEsBuf>, "sceMpegMallocAvcEsBuf", 'i', "x" },
|
|
{0XCEB870B1, &WrapI_UI<sceMpegFreeAvcEsBuf>, "sceMpegFreeAvcEsBuf", 'i', "xi" },
|
|
{0X167AFD9E, &WrapI_UUU<sceMpegInitAu>, "sceMpegInitAu", 'i', "xxx" },
|
|
{0X682A619B, &WrapU_V<sceMpegInit>, "sceMpegInit", 'x', "" },
|
|
{0X606A4649, &WrapI_U<sceMpegDelete>, "sceMpegDelete", 'i', "x" },
|
|
{0X874624D6, &WrapU_V<sceMpegFinish>, "sceMpegFinish", 'x', "" },
|
|
{0X800C44DF, &WrapU_UUUI<sceMpegAtracDecode>, "sceMpegAtracDecode", 'x', "xxxi" },
|
|
{0X0E3C2E9D, &WrapU_UUUUU<sceMpegAvcDecode>, "sceMpegAvcDecode", 'x', "xxxxx" },
|
|
{0X740FCCD1, &WrapU_UUUU<sceMpegAvcDecodeStop>, "sceMpegAvcDecodeStop", 'x', "xxxx" },
|
|
{0X4571CC64, &WrapU_U<sceMpegAvcDecodeFlush>, "sceMpegAvcDecodeFlush", 'x', "x" },
|
|
{0X0F6C18D7, &WrapI_UU<sceMpegAvcDecodeDetail>, "sceMpegAvcDecodeDetail", 'i', "xx" },
|
|
{0XA11C7026, &WrapI_UU<sceMpegAvcDecodeMode>, "sceMpegAvcDecodeMode", 'i', "xx" },
|
|
{0X37295ED8, &WrapU_UUUUUU<sceMpegRingbufferConstruct>, "sceMpegRingbufferConstruct", 'x', "xxxxxx" },
|
|
{0X13407F13, &WrapU_U<sceMpegRingbufferDestruct>, "sceMpegRingbufferDestruct", 'x', "x" },
|
|
{0XB240A59E, &WrapU_UUU<sceMpegRingbufferPut>, "sceMpegRingbufferPut", 'x', "xxx" },
|
|
{0XB5F6DC87, &WrapI_U<sceMpegRingbufferAvailableSize>, "sceMpegRingbufferAvailableSize", 'i', "x" },
|
|
{0XD7A29F46, &WrapU_I<sceMpegRingbufferQueryMemSize>, "sceMpegRingbufferQueryMemSize", 'x', "i" },
|
|
{0X769BEBB6, &WrapI_U<sceMpegRingbufferQueryPackNum>, "sceMpegRingbufferQueryPackNum", 'i', "x" },
|
|
{0X211A057C, &WrapI_UUUUU<sceMpegAvcQueryYCbCrSize>, "sceMpegAvcQueryYCbCrSize", 'i', "xxxxx" },
|
|
{0XF0EB1125, &WrapI_UUUU<sceMpegAvcDecodeYCbCr>, "sceMpegAvcDecodeYCbCr", 'i', "xxxx" },
|
|
{0XF2930C9C, &WrapU_UUU<sceMpegAvcDecodeStopYCbCr>, "sceMpegAvcDecodeStopYCbCr", 'x', "xxx" },
|
|
{0X67179B1B, &WrapU_UIIIU<sceMpegAvcInitYCbCr>, "sceMpegAvcInitYCbCr", 'x', "xiiix" },
|
|
{0X0558B075, &WrapU_UUU<sceMpegAvcCopyYCbCr>, "sceMpegAvcCopyYCbCr", 'x', "xxx" },
|
|
{0X31BD0272, &WrapU_UUUIU<sceMpegAvcCsc>, "sceMpegAvcCsc", 'x', "xxxix" },
|
|
{0X9DCFB7EA, &WrapU_UII<sceMpegChangeGetAuMode>, "sceMpegChangeGetAuMode", 'x', "xii" },
|
|
{0X8C1E027D, &WrapU_UIUU<sceMpegGetPcmAu>, "sceMpegGetPcmAu", 'x', "xixx" },
|
|
{0XC02CF6B5, &WrapI_UUU<sceMpegQueryPcmEsSize>, "sceMpegQueryPcmEsSize", 'i', "xxx" },
|
|
{0XC45C99CC, &WrapU_UUU<sceMpegQueryUserdataEsSize>, "sceMpegQueryUserdataEsSize", 'x', "xxx" },
|
|
{0X234586AE, &WrapU_UUI<sceMpegChangeGetAvcAuMode>, "sceMpegChangeGetAvcAuMode", 'x', "xxi" },
|
|
{0X63B9536A, &WrapU_U<sceMpegAvcResourceGetAvcDecTopAddr>, "sceMpegAvcResourceGetAvcDecTopAddr", 'x', "x" },
|
|
{0X8160A2FE, &WrapU_U<sceMpegAvcResourceFinish>, "sceMpegAvcResourceFinish", 'x', "x" },
|
|
{0XAF26BB01, &WrapU_U<sceMpegAvcResourceGetAvcEsBuf>, "sceMpegAvcResourceGetAvcEsBuf", 'x', "x" },
|
|
{0XFCBDB5AD, &WrapU_U<sceMpegAvcResourceInit>, "sceMpegAvcResourceInit", 'x', "x" },
|
|
{0XF5E7EA31, &WrapI_UUUI<sceMpegAvcConvertToYuv420>, "sceMpegAvcConvertToYuv420", 'i', "xxxi" },
|
|
{0X01977054, &WrapI_UUUU<sceMpegGetUserdataAu>, "sceMpegGetUserdataAu", 'i', "xxxx" },
|
|
{0X3C37A7A6, &WrapU_UU<sceMpegNextAvcRpAu>, "sceMpegNextAvcRpAu", 'x', "xx" },
|
|
{0X11F95CF1, &WrapU_U<sceMpegGetAvcNalAu>, "sceMpegGetAvcNalAu", 'x', "x" },
|
|
{0XAB0E9556, &WrapU_U<sceMpegAvcDecodeDetailIndex>, "sceMpegAvcDecodeDetailIndex", 'x', "x" },
|
|
{0XCF3547A2, &WrapU_U<sceMpegAvcDecodeDetail2>, "sceMpegAvcDecodeDetail2", 'x', "x" },
|
|
{0X921FCCCF, &WrapU_U<sceMpegGetAvcEsAu>, "sceMpegGetAvcEsAu", 'x', "x" },
|
|
{0XE95838F6, &WrapU_U<sceMpegAvcCscInfo>, "sceMpegAvcCscInfo", 'x', "x" },
|
|
{0XD1CE4950, &WrapU_U<sceMpegAvcCscMode>, "sceMpegAvcCscMode", 'x', "x" },
|
|
{0XDBB60658, &WrapU_U<sceMpegFlushAu>, "sceMpegFlushAu", 'x', "x" },
|
|
{0XD4DD6E75, nullptr, "sceMpeg_D4DD6E75", '?', "" },
|
|
{0X11CAB459, nullptr, "sceMpeg_11CAB459", '?', "" },
|
|
{0XC345DED2, nullptr, "sceMpeg_C345DED2", '?', "" },
|
|
{0XB27711A8, nullptr, "sceMpeg_B27711A8", '?', "" },
|
|
{0X988E9E12, nullptr, "sceMpeg_988E9E12", '?', "" },
|
|
};
|
|
|
|
void Register_sceMpeg()
|
|
{
|
|
RegisterModule("sceMpeg", ARRAY_SIZE(sceMpeg), sceMpeg);
|
|
}
|
|
|
|
// This function is currently only been used for PMP videos
|
|
// p pointing to a SceMpegLLI structure consists of video frame blocks.
|
|
static u32 sceMpegbase_BEA18F91(u32 p)
|
|
{
|
|
pmp_videoSource = p;
|
|
pmp_nBlocks = 0;
|
|
SceMpegLLI lli;
|
|
while (1){
|
|
Memory::ReadStruct(p, &lli);
|
|
pmp_nBlocks++;
|
|
// lli.Next ==0 for last block
|
|
if (lli.Next == 0){
|
|
break;
|
|
}
|
|
p = p + sizeof(SceMpegLLI);
|
|
}
|
|
|
|
DEBUG_LOG(ME, "sceMpegbase_BEA18F91(%08x), received %d block(s)", pmp_videoSource, pmp_nBlocks);
|
|
return 0;
|
|
}
|
|
|
|
const HLEFunction sceMpegbase[] =
|
|
{
|
|
{0XBEA18F91, &WrapU_U<sceMpegbase_BEA18F91>, "sceMpegbase_BEA18F91", 'x', "x" },
|
|
{0X492B5E4B, nullptr, "sceMpegBaseCscInit", '?', "" },
|
|
{0X0530BE4E, nullptr, "sceMpegbase_0530BE4E", '?', "" },
|
|
{0X91929A21, nullptr, "sceMpegBaseCscAvc", '?', "" },
|
|
{0X304882E1, nullptr, "sceMpegBaseCscAvcRange", '?', "" },
|
|
{0X7AC0321A, nullptr, "sceMpegBaseYCrCbCopy", '?', "" }
|
|
};
|
|
|
|
void Register_sceMpegbase()
|
|
{
|
|
RegisterModule("sceMpegbase", ARRAY_SIZE(sceMpegbase), sceMpegbase);
|
|
};
|