mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-22 12:26:21 +02:00
655 lines
23 KiB
C++
655 lines
23 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/.
|
|
|
|
// sceVideocodec - the H.264 decoding interface the Media Engine exposes.
|
|
//
|
|
// This exists so that flash0:/kd/mpeg.prx can be run in place of our sceMpeg HLE: mpeg.prx needs
|
|
// only sceVideocodec, sceMpegbase and sceAudiocodec from us, and the other two we already have.
|
|
// The point is to have a reference to compare the HLE against, so it aims to behave like the
|
|
// hardware rather than to be the fastest way to get pixels on screen.
|
|
//
|
|
// Behaviour cross-checked against JPCSP, whose description of the buffer layout was established
|
|
// by looking at sceMpegBaseYCrCbCopy output on a real PSP.
|
|
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <vector>
|
|
|
|
#include "Common/Serialize/Serializer.h"
|
|
#include "Common/Serialize/SerializeFuncs.h"
|
|
#include "Core/HLE/ErrorCodes.h"
|
|
#include "Core/HLE/HLE.h"
|
|
#include "Core/HLE/FunctionWrappers.h"
|
|
#include "Core/HLE/sceVideocodec.h"
|
|
#include "Core/Util/BlockAllocator.h"
|
|
#include "Core/HLE/sceMpeg.h"
|
|
#include "Core/HLE/sceMpegbase.h"
|
|
#include "Core/HW/AvcDecoder.h"
|
|
#include "Core/MemMap.h"
|
|
#include "Core/MIPS/MIPS.h"
|
|
|
|
// The context the caller hands us is 96 bytes. The offsets below are what mpeg.prx actually
|
|
// reads and writes; anything not listed here it doesn't look at.
|
|
enum {
|
|
CTX_MAGIC = 0, // 0x05100601, same marker sceAudiocodec's context carries
|
|
CTX_VERSION = 4, // GetVersion writes 0x78 here
|
|
CTX_STATUS = 8,
|
|
CTX_MEM = 12,
|
|
CTX_OUT_INFO = 16, // pointer to the 108-byte result descriptor
|
|
CTX_EDRAM = 20,
|
|
CTX_EDRAM_SIZE = 24,
|
|
CTX_AU_DATA = 36, // the access unit to decode
|
|
CTX_AU_SIZE = 40,
|
|
CTX_YUV_STRUCT = 44, // type 0: pointer to the eight output buffers
|
|
CTX_EDRAM_RAW = 92,
|
|
};
|
|
|
|
// Fields of the descriptor at CTX_OUT_INFO that mpeg.prx reads back.
|
|
enum {
|
|
OUT_DATA = 0,
|
|
OUT_SIZE = 4,
|
|
OUT_UNK8 = 8,
|
|
OUT_UNK12 = 12,
|
|
OUT_CONSUMED = 44,
|
|
OUT_WIDTH = 48,
|
|
OUT_HEIGHT = 52,
|
|
OUT_FRAME_READY = 60, // 2 when a frame came out, 1 when it didn't
|
|
OUT_UNK64 = 64,
|
|
OUT_UNK72 = 72,
|
|
OUT_TIMESTAMP = 76,
|
|
OUT_FPS = 80,
|
|
OUT_BUFFER_Y = 84,
|
|
OUT_BUFFER_CR = 88,
|
|
OUT_BUFFER_CB = 92,
|
|
OUT_WIDTH_Y = 96,
|
|
OUT_WIDTH_CR = 100,
|
|
OUT_WIDTH_CB = 104,
|
|
};
|
|
|
|
// A game can have more than one of these open at once - Silent Hill Origins runs two, one that
|
|
// owns the EDRAM and one that does the decoding - so everything here is per context.
|
|
struct VideocodecCtx {
|
|
AvcDecoder *decoder = nullptr;
|
|
int type = 0;
|
|
int frameCount = 0;
|
|
// What sceVideocodecGetEDRAM handed out, as an address in g_meRam.
|
|
u32 edram = 0;
|
|
// The frame buffers the ME reported back, also in g_meRam - see PublishFrameBuffers.
|
|
u32 frameBuffers = 0;
|
|
u32 frameBuffersSize = 0;
|
|
int frameBufferWidth = 0;
|
|
int frameBufferHeight = 0;
|
|
};
|
|
|
|
static std::map<u32, VideocodecCtx> g_videocodecCtxs;
|
|
|
|
// The Media Engine's own 2MB of embedded DRAM, modelled as memory of ours.
|
|
//
|
|
// The main CPU cannot address it. mpeg.prx asks for a block with sceVideocodecGetEDRAM, keeps the value
|
|
// and hands it back, and never dereferences it; the frame buffers the ME reports back live in here
|
|
// too, which is why sceVideocodecSetMemory is given a frame size rather than a buffer - 480, 272
|
|
// and a count of 2 for a full-screen movie, with nowhere for the caller to say where to put them.
|
|
//
|
|
// The addresses handed out are offsets into g_meRam, based well outside anything PSP RAM maps so
|
|
// that a stray dereference faults where it happens instead of quietly reading the game's memory.
|
|
// Being outside PSP RAM, the contents aren't in the memory a savestate captures either, so the
|
|
// block and its allocator go in __VideocodecDoState.
|
|
static const u32 ME_EDRAM_BASE = 0xC0000000;
|
|
static const u32 ME_EDRAM_SIZE = 2 * 1024 * 1024;
|
|
static std::vector<u8> g_meRam;
|
|
static BlockAllocator g_meAlloc(64);
|
|
|
|
// The 2MB is only committed once something asks for a piece of it, so a game that never plays a
|
|
// video pays nothing for this and its savestates don't carry it.
|
|
static void MEEnsureRam() {
|
|
if (g_meRam.size() != ME_EDRAM_SIZE) {
|
|
g_meRam.assign(ME_EDRAM_SIZE, 0);
|
|
g_meAlloc.Init(ME_EDRAM_BASE, ME_EDRAM_SIZE, false);
|
|
}
|
|
}
|
|
|
|
u8 *VideocodecMEPointer(u32 addr, u32 size) {
|
|
if (addr < ME_EDRAM_BASE || size > ME_EDRAM_SIZE || g_meRam.size() != ME_EDRAM_SIZE) {
|
|
return nullptr;
|
|
}
|
|
const u32 offset = addr - ME_EDRAM_BASE;
|
|
if (offset > ME_EDRAM_SIZE - size) {
|
|
return nullptr;
|
|
}
|
|
return g_meRam.data() + offset;
|
|
}
|
|
|
|
static void FreeContext(VideocodecCtx &ctx) {
|
|
delete ctx.decoder;
|
|
ctx.decoder = nullptr;
|
|
if (ctx.edram) {
|
|
g_meAlloc.Free(ctx.edram);
|
|
ctx.edram = 0;
|
|
}
|
|
if (ctx.frameBuffers) {
|
|
g_meAlloc.Free(ctx.frameBuffers);
|
|
ctx.frameBuffers = 0;
|
|
}
|
|
}
|
|
|
|
// freeMemory is false when loading a savestate: the state carries its own g_meAlloc, so the blocks
|
|
// these contexts were holding belong to a world that no longer exists and freeing them would be
|
|
// freeing someone else's memory.
|
|
static void ClearContexts(bool freeMemory) {
|
|
for (auto &[addr, ctx] : g_videocodecCtxs) {
|
|
if (freeMemory) {
|
|
FreeContext(ctx);
|
|
} else {
|
|
delete ctx.decoder;
|
|
ctx.decoder = nullptr;
|
|
}
|
|
}
|
|
g_videocodecCtxs.clear();
|
|
}
|
|
|
|
void __VideocodecInit() {
|
|
// Nothing to free: a boot starts with a fresh allocator.
|
|
g_videocodecCtxs.clear();
|
|
g_meRam.clear();
|
|
g_meRam.shrink_to_fit();
|
|
g_meAlloc.Shutdown();
|
|
}
|
|
|
|
void __VideocodecShutdown() {
|
|
ClearContexts(true);
|
|
}
|
|
|
|
void __VideocodecDoState(PointerWrap &p) {
|
|
auto s = p.Section("sceVideocodec", 0, 1);
|
|
if (!s) {
|
|
return;
|
|
}
|
|
|
|
// The decoders themselves aren't serializable - a savestate resumes with fresh ones, which
|
|
// costs at most the frames up to the next keyframe. The frame buffer allocations do have to
|
|
// come back, or we'd lose track of memory the restored allocator still has handed out.
|
|
//
|
|
// If we in the future directly integrate with a h.264 decoder, it might be actually possible
|
|
// to serialize the internal states. But 100% accurate savestates during cutscene playback are
|
|
// not really that important.
|
|
int count = (int)g_videocodecCtxs.size();
|
|
Do(p, count);
|
|
if (p.mode == p.MODE_READ) {
|
|
ClearContexts(false);
|
|
for (int i = 0; i < count; i++) {
|
|
u32 addr = 0;
|
|
VideocodecCtx ctx;
|
|
Do(p, addr);
|
|
Do(p, ctx.type);
|
|
Do(p, ctx.frameCount);
|
|
Do(p, ctx.frameBuffers);
|
|
Do(p, ctx.frameBuffersSize);
|
|
Do(p, ctx.frameBufferWidth);
|
|
Do(p, ctx.frameBufferHeight);
|
|
Do(p, ctx.edram);
|
|
g_videocodecCtxs[addr] = std::move(ctx);
|
|
}
|
|
} else {
|
|
for (auto &[addr, ctx] : g_videocodecCtxs) {
|
|
u32 a = addr;
|
|
Do(p, a);
|
|
Do(p, ctx.type);
|
|
Do(p, ctx.frameCount);
|
|
Do(p, ctx.frameBuffers);
|
|
Do(p, ctx.frameBuffersSize);
|
|
Do(p, ctx.frameBufferWidth);
|
|
Do(p, ctx.frameBufferHeight);
|
|
Do(p, ctx.edram);
|
|
}
|
|
}
|
|
|
|
// The Media Engine's memory and who holds what of it. Empty until a video plays, and then it
|
|
// is the one copy - the contexts above only carry addresses into it.
|
|
Do(p, g_meRam);
|
|
g_meAlloc.DoState(p);
|
|
}
|
|
|
|
u32 VideocodecFrameBufferLayout(int width, int height, int sizes[8], u32 offsets[8]) {
|
|
// buffer0/2 take the odd band out when the width isn't a multiple of 32.
|
|
const int lumaLeft = ((width + 16) >> 5) * (height >> 1) * 16;
|
|
const int lumaRight = (width >> 5) * (height >> 1) * 16;
|
|
const int local[8] = {
|
|
lumaLeft, lumaRight, lumaLeft, lumaRight,
|
|
lumaLeft >> 1, lumaLeft >> 1, lumaRight >> 1, lumaRight >> 1,
|
|
};
|
|
u32 total = 0;
|
|
for (int i = 0; i < 8; i++) {
|
|
if (sizes) {
|
|
sizes[i] = local[i];
|
|
}
|
|
if (offsets) {
|
|
offsets[i] = total;
|
|
}
|
|
total += (local[i] + 63) & ~63;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
// The descriptor mpeg.prx passes in is empty: on hardware the ME owns the frame buffers, and
|
|
// reports where it put them. So allocate them here and fill the descriptor in the shape
|
|
// sceMpegBaseCscAvc expects - dimensions in macroblocks, then the eight buffer addresses.
|
|
static bool PublishFrameBuffers(VideocodecCtx &vctx, u32 structAddr, int width, int height, u32 buffers[8]) {
|
|
u32 offsets[8];
|
|
const u32 total = VideocodecFrameBufferLayout(width, height, nullptr, offsets);
|
|
if (total == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (vctx.frameBuffers && (width != vctx.frameBufferWidth || height != vctx.frameBufferHeight)) {
|
|
g_meAlloc.Free(vctx.frameBuffers);
|
|
vctx.frameBuffers = 0;
|
|
}
|
|
if (!vctx.frameBuffers) {
|
|
MEEnsureRam();
|
|
u32 size = total;
|
|
vctx.frameBuffers = g_meAlloc.Alloc(size, false, "VideocodecFrame");
|
|
if (vctx.frameBuffers == (u32)-1) {
|
|
vctx.frameBuffers = 0;
|
|
ERROR_LOG(Log::ME, "sceVideocodec: no room in ME memory for %d bytes of frame buffers", total);
|
|
return false;
|
|
}
|
|
vctx.frameBuffersSize = total;
|
|
vctx.frameBufferWidth = width;
|
|
vctx.frameBufferHeight = height;
|
|
INFO_LOG(Log::ME, "sceVideocodec: %d bytes of frame buffers at %08x for %dx%d",
|
|
total, vctx.frameBuffers, width, height);
|
|
}
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
buffers[i] = vctx.frameBuffers + offsets[i];
|
|
}
|
|
|
|
if (!Memory::IsValidRange(structAddr, 48)) {
|
|
return false;
|
|
}
|
|
Memory::WriteUnchecked_U32(height >> 4, structAddr + 0); // macroblocks
|
|
Memory::WriteUnchecked_U32(width >> 4, structAddr + 4);
|
|
for (int i = 0; i < 8; i++) {
|
|
Memory::WriteUnchecked_U32(buffers[i], structAddr + 16 + i * 4);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void VideocodecGetCtxInfo(std::vector<VideocodecCtxInfo> *infos) {
|
|
infos->clear();
|
|
for (const auto &[addr, ctx] : g_videocodecCtxs) {
|
|
VideocodecCtxInfo info;
|
|
info.ctxAddr = addr;
|
|
info.type = ctx.type;
|
|
info.hasDecoder = ctx.decoder != nullptr;
|
|
info.frameCount = ctx.frameCount;
|
|
// Hardware keeps the token in the context struct, so that's where we read it back from too.
|
|
info.edramToken = ctx.edram;
|
|
info.edramSize = ctx.edram ? g_meAlloc.GetBlockSizeFromAddress(ctx.edram) : 0;
|
|
info.frameBuffers = ctx.frameBuffers;
|
|
info.frameBuffersSize = ctx.frameBuffersSize;
|
|
info.width = ctx.frameBufferWidth;
|
|
info.height = ctx.frameBufferHeight;
|
|
infos->push_back(info);
|
|
}
|
|
}
|
|
|
|
bool VideocodecGetFrameBuffers(u32 firstBuffer, u32 buffers[8]) {
|
|
// sceMpegbase only has the first of the eight addresses, so find whose allocation it is.
|
|
const VideocodecCtx *found = nullptr;
|
|
for (const auto &[addr, ctx] : g_videocodecCtxs) {
|
|
if (ctx.frameBuffers && ctx.frameBuffers == firstBuffer) {
|
|
found = &ctx;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
return false;
|
|
}
|
|
u32 offsets[8];
|
|
VideocodecFrameBufferLayout(found->frameBufferWidth, found->frameBufferHeight, nullptr, offsets);
|
|
for (int i = 0; i < 8; i++) {
|
|
buffers[i] = found->frameBuffers + offsets[i];
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Writes the decoded frame into the eight buffers the hardware uses. The image is in 32-pixel
|
|
// vertical bands split into two 16-pixel halves, and which buffer a row lands in depends on
|
|
// whether it is even or odd. This is the exact inverse of ReadTiledYCbCr in sceMpeg.cpp, which
|
|
// is what reads it back out - see the comment there for the full layout.
|
|
static void WriteTiledYCbCr(const u32 *buffers, const AvcDecoder &dec, int width, int height) {
|
|
const int width2 = width >> 1;
|
|
const int height2 = height >> 1;
|
|
|
|
const u8 *srcY = dec.Plane(0);
|
|
const u8 *srcCb = dec.Plane(1);
|
|
const u8 *srcCr = dec.Plane(2);
|
|
const int strideY = dec.Stride(0);
|
|
const int strideCb = dec.Stride(1);
|
|
const int strideCr = dec.Stride(2);
|
|
if (!srcY || !srcCb || !srcCr) {
|
|
return;
|
|
}
|
|
|
|
int sizes[8];
|
|
VideocodecFrameBufferLayout(width, height, sizes, nullptr);
|
|
const int *ySize = sizes;
|
|
const int *cSize = sizes + 4;
|
|
|
|
for (int b = 0; b < 4; b++) {
|
|
if (ySize[b] <= 0) {
|
|
continue;
|
|
}
|
|
u8 *dst = VideocodecMEPointer(buffers[b], ySize[b]);
|
|
if (!dst) {
|
|
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(dst + j, srcY + (size_t)row * strideY + bandX, run);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int b = 0; b < 4; b++) {
|
|
if (cSize[b] <= 0) {
|
|
continue;
|
|
}
|
|
u8 *dst = VideocodecMEPointer(buffers[4 + b], cSize[b]);
|
|
if (!dst) {
|
|
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;
|
|
}
|
|
dst[j] = srcCb[(size_t)row * strideCb + x];
|
|
dst[j + 1] = srcCr[(size_t)row * strideCr + x];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int sceVideocodecOpen(u32 ctxAddr, int type) {
|
|
if (!Memory::IsValidRange(ctxAddr, 96)) {
|
|
return hleLogError(Log::ME, -1, "bad context pointer");
|
|
}
|
|
Memory::WriteUnchecked_U32(0x05100601, ctxAddr + CTX_MAGIC);
|
|
if (!AvcDecoder::IsAvailable()) {
|
|
return hleLogError(Log::ME, -1, "built without ffmpeg, can't decode video");
|
|
}
|
|
g_videocodecCtxs[ctxAddr].type = type;
|
|
return hleLogInfo(Log::ME, 0, "type %d", type);
|
|
}
|
|
|
|
static int sceVideocodecInit(u32 ctxAddr, int type) {
|
|
if (!Memory::IsValidRange(ctxAddr, 96)) {
|
|
return hleLogError(Log::ME, -1, "bad context pointer");
|
|
}
|
|
Memory::WriteUnchecked_U32(Memory::ReadUnchecked_U32(ctxAddr + CTX_EDRAM) + 8, ctxAddr + CTX_MEM);
|
|
VideocodecCtx &vctx = g_videocodecCtxs[ctxAddr];
|
|
delete vctx.decoder;
|
|
vctx.decoder = new AvcDecoder();
|
|
vctx.frameCount = 0;
|
|
vctx.type = type;
|
|
return hleLogInfo(Log::ME, 0, "type %d", type);
|
|
}
|
|
|
|
// See g_meRam for why this doesn't come out of the game's memory.
|
|
static int sceVideocodecGetEDRAM(u32 ctxAddr, int type) {
|
|
if (!Memory::IsValidRange(ctxAddr, 96)) {
|
|
return hleLogError(Log::ME, -1, "bad context pointer");
|
|
}
|
|
// The firmware refuses rather than replacing one it already handed out, and a game that asks
|
|
// twice would otherwise leave the first block with nothing pointing at it.
|
|
if (Memory::ReadUnchecked_U32(ctxAddr + CTX_EDRAM_RAW) != 0) {
|
|
return hleLogError(Log::ME, SCE_MPEG_ERROR_AVC_INVALID_VALUE, "context already has EDRAM");
|
|
}
|
|
// Rounding as the firmware does it - the OR really is an OR, so every size ends in 0x3F.
|
|
u32 size = (Memory::ReadUnchecked_U32(ctxAddr + CTX_EDRAM_SIZE) + 63) | 0x3F;
|
|
MEEnsureRam();
|
|
const u32 addr = g_meAlloc.Alloc(size, false, "VideocodecEDRAM");
|
|
if (addr == (u32)-1) {
|
|
return hleLogError(Log::ME, SCE_MPEG_ERROR_AVC_INVALID_VALUE, "no room in ME memory for %u bytes", size);
|
|
}
|
|
g_videocodecCtxs[ctxAddr].edram = addr;
|
|
// Both fields as hardware fills them: the raw value and the 64-byte-aligned one. The allocator
|
|
// works in 64-byte grains, so the two only differ in what they mean, not in value.
|
|
Memory::WriteUnchecked_U32(addr, ctxAddr + CTX_EDRAM);
|
|
Memory::WriteUnchecked_U32(addr, ctxAddr + CTX_EDRAM_RAW);
|
|
return hleLogInfo(Log::ME, 0, "%u bytes at %08x in ME memory", size, addr);
|
|
}
|
|
|
|
static int sceVideocodecReleaseEDRAM(u32 ctxAddr) {
|
|
if (!Memory::IsValidRange(ctxAddr, 96)) {
|
|
return hleLogError(Log::ME, -1, "bad context pointer");
|
|
}
|
|
// Whether this context has one is recorded in the context struct, as on hardware.
|
|
const u32 token = Memory::ReadUnchecked_U32(ctxAddr + CTX_EDRAM_RAW);
|
|
if (!token) {
|
|
return hleLogError(Log::ME, SCE_MPEG_ERROR_AVC_INVALID_VALUE, "context has no EDRAM");
|
|
}
|
|
auto it = g_videocodecCtxs.find(ctxAddr);
|
|
if (it != g_videocodecCtxs.end() && it->second.edram) {
|
|
g_meAlloc.Free(it->second.edram);
|
|
it->second.edram = 0;
|
|
}
|
|
Memory::WriteUnchecked_U32(0, ctxAddr + CTX_EDRAM);
|
|
Memory::WriteUnchecked_U32(0, ctxAddr + CTX_EDRAM_RAW);
|
|
return hleLogInfo(Log::ME, 0, "released %08x", token);
|
|
}
|
|
|
|
static int sceVideocodecDecode(u32 ctxAddr, int type) {
|
|
if (!Memory::IsValidRange(ctxAddr, 96)) {
|
|
return hleLogError(Log::ME, -1, "bad context pointer");
|
|
}
|
|
if (type != 0 && type != 1) {
|
|
return hleLogError(Log::ME, -1, "unknown type %d", type);
|
|
}
|
|
// Only Open and Init create contexts. Keying off whatever address Decode is handed would let
|
|
// a game that never opens one accumulate decoders that nothing ever deletes.
|
|
auto ctxIter = g_videocodecCtxs.find(ctxAddr);
|
|
if (ctxIter == g_videocodecCtxs.end()) {
|
|
return hleLogError(Log::ME, -1, "decode on a context that was never opened");
|
|
}
|
|
VideocodecCtx &vctx = ctxIter->second;
|
|
if (!vctx.decoder) {
|
|
vctx.decoder = new AvcDecoder();
|
|
}
|
|
|
|
const u32 auAddr = Memory::ReadUnchecked_U32(ctxAddr + CTX_AU_DATA);
|
|
const int auSize = (int)Memory::ReadUnchecked_U32(ctxAddr + CTX_AU_SIZE);
|
|
const u32 outAddr = Memory::ReadUnchecked_U32(ctxAddr + CTX_OUT_INFO);
|
|
Memory::WriteUnchecked_U32(0, ctxAddr + CTX_STATUS);
|
|
|
|
if (!Memory::IsValidRange(outAddr, 108)) {
|
|
return hleLogError(Log::ME, -1, "bad output descriptor");
|
|
}
|
|
|
|
// The access unit address mpeg.prx passes is in Media Engine space, which we can't read -
|
|
// on hardware sceMpegBasePESpacketCopy DMA'd the data there. That copy is ours, so use what
|
|
// it gathered instead, and fall back to main memory for any caller that points at it
|
|
// directly.
|
|
bool gotFrame = false;
|
|
const u8 *au = nullptr;
|
|
int auBytes = 0;
|
|
// Owns the gathered payload for as long as au points into it.
|
|
std::vector<u8> pes;
|
|
if (auSize > 0 && Memory::IsValidRange(auAddr, auSize)) {
|
|
au = Memory::GetTypedPointerRange<u8>(auAddr, auSize);
|
|
auBytes = auSize;
|
|
} else {
|
|
// Take the payload copied to this exact address - the same call carries audio too.
|
|
pes = MpegBaseTakePESPacket(auAddr);
|
|
if (!pes.empty()) {
|
|
au = pes.data();
|
|
auBytes = (int)pes.size();
|
|
}
|
|
}
|
|
if (au && auBytes > 0) {
|
|
gotFrame = vctx.decoder->Decode(au, auBytes);
|
|
}
|
|
|
|
const int width = gotFrame ? vctx.decoder->Width() : 0;
|
|
const int height = gotFrame ? vctx.decoder->Height() : 0;
|
|
|
|
auto out32 = [outAddr](int offset, u32 value) {
|
|
Memory::WriteUnchecked_U32(value, outAddr + offset);
|
|
};
|
|
|
|
// Only the type 1 path fills the descriptor in. For type 0 the YCbCr descriptor sits just
|
|
// 0x40 bytes after this one - mpeg.prx allocates them adjacently - so writing the type 1
|
|
// fields here scribbles over the buffer addresses the colour conversion is about to read.
|
|
// For type 0 the frame isn't announced until the buffers holding it have been published -
|
|
// see below. Saying "one image decoded" and then failing to allocate would have mpeg.prx
|
|
// convert from whatever the descriptor pointed at last.
|
|
bool published = false;
|
|
if (type == 0) {
|
|
out32(8, width);
|
|
out32(12, height);
|
|
out32(28, 1);
|
|
} else {
|
|
out32(OUT_DATA, auAddr);
|
|
out32(OUT_SIZE, auSize);
|
|
out32(OUT_UNK12, 0x40);
|
|
out32(OUT_CONSUMED, auSize);
|
|
out32(OUT_WIDTH, width);
|
|
out32(OUT_HEIGHT, height);
|
|
out32(OUT_FRAME_READY, gotFrame ? 2 : 1);
|
|
out32(OUT_UNK64, 1);
|
|
out32(OUT_UNK72, (u32)-1);
|
|
out32(OUT_TIMESTAMP, vctx.frameCount * 0x64);
|
|
out32(OUT_FPS, 2997);
|
|
}
|
|
|
|
if (gotFrame) {
|
|
vctx.frameCount++;
|
|
if (type == 0) {
|
|
const u32 yuvStructAddr = Memory::ReadUnchecked_U32(ctxAddr + CTX_YUV_STRUCT);
|
|
if (Memory::IsValidRange(yuvStructAddr, 8 * 4)) {
|
|
u32 buffers[8];
|
|
if (PublishFrameBuffers(vctx, yuvStructAddr, width, height, buffers)) {
|
|
WriteTiledYCbCr(buffers, *vctx.decoder, width, height);
|
|
published = true;
|
|
}
|
|
} else {
|
|
WARN_LOG(Log::ME, "sceVideocodecDecode: type 0 without a usable buffer list");
|
|
}
|
|
} else {
|
|
// Type 1 hands back plain planar YUV, so just point at the decoder's own planes -
|
|
// nothing in the descriptor is read until the caller copies from them.
|
|
out32(OUT_WIDTH_Y, width);
|
|
out32(OUT_WIDTH_CR, width / 2);
|
|
out32(OUT_WIDTH_CB, width / 2);
|
|
}
|
|
}
|
|
|
|
if (type == 0) {
|
|
out32(32, published ? 1 : 0); // images decoded - mpeg.prx won't convert without this
|
|
out32(36, published ? 0 : 1);
|
|
}
|
|
|
|
return hleLogDebug(Log::ME, 0, "type %d, %d bytes -> %s %dx%d",
|
|
type, auBytes, gotFrame ? "frame" : "no frame yet", width, height);
|
|
}
|
|
|
|
static int sceVideocodecStop(u32 ctxAddr, int type) {
|
|
auto it = g_videocodecCtxs.find(ctxAddr);
|
|
if (it != g_videocodecCtxs.end() && it->second.decoder) {
|
|
it->second.decoder->Flush();
|
|
}
|
|
return hleLogInfo(Log::ME, 0);
|
|
}
|
|
|
|
static int sceVideocodecDelete(u32 ctxAddr, int type) {
|
|
auto it = g_videocodecCtxs.find(ctxAddr);
|
|
if (it != g_videocodecCtxs.end()) {
|
|
FreeContext(it->second);
|
|
g_videocodecCtxs.erase(it);
|
|
}
|
|
return hleLogInfo(Log::ME, 0);
|
|
}
|
|
|
|
static int sceVideocodecGetVersion(u32 ctxAddr, int type) {
|
|
if (!Memory::IsValidRange(ctxAddr, 96)) {
|
|
return hleLogError(Log::ME, -1, "bad context pointer");
|
|
}
|
|
// The value a real PSP returns, read with JpcspTrace.
|
|
Memory::WriteUnchecked_U32(0x78, ctxAddr + CTX_VERSION);
|
|
return hleLogInfo(Log::ME, 0);
|
|
}
|
|
|
|
static int sceVideocodecGetSEI(u32 ctxAddr, int type) {
|
|
return hleLogWarning(Log::ME, 0, "UNIMPL");
|
|
}
|
|
|
|
static int sceVideocodecScanHeader(u32 ctxAddr, int type) {
|
|
return hleLogWarning(Log::ME, 0, "UNIMPL");
|
|
}
|
|
|
|
static int sceVideocodecGetFrameCrop(u32 ctxAddr, int type) {
|
|
return hleLogWarning(Log::ME, 0, "UNIMPL");
|
|
}
|
|
|
|
static int sceVideocodecSetMemory(u32 ctxAddr, int type) {
|
|
return hleLogDebug(Log::ME, 0);
|
|
}
|
|
|
|
static int sceVideocodec_893B32B1(u32 ctxAddr, int type) {
|
|
return hleLogWarning(Log::ME, 0, "UNIMPL");
|
|
}
|
|
|
|
static int sceVideocodec_D95C24D5(u32 ctxAddr, int type) {
|
|
return hleLogWarning(Log::ME, 0, "UNIMPL");
|
|
}
|
|
|
|
const HLEFunction sceVideocodec[] = {
|
|
{0XC01EC829, &WrapI_UI<sceVideocodecOpen>, "sceVideocodecOpen", 'i', "xi"},
|
|
{0X2D31F5B1, &WrapI_UI<sceVideocodecGetEDRAM>, "sceVideocodecGetEDRAM", 'i', "xi"},
|
|
{0X17099F0A, &WrapI_UI<sceVideocodecInit>, "sceVideocodecInit", 'i', "xi"},
|
|
{0XDBA273FA, &WrapI_UI<sceVideocodecDecode>, "sceVideocodecDecode", 'i', "xi"},
|
|
{0X4F160BF4, &WrapI_U<sceVideocodecReleaseEDRAM>, "sceVideocodecReleaseEDRAM", 'i', "x" },
|
|
{0X745A7B7A, &WrapI_UI<sceVideocodecSetMemory>, "sceVideocodecSetMemory", 'i', "xi"},
|
|
{0X2F385E7F, &WrapI_UI<sceVideocodecScanHeader>, "sceVideocodecScanHeader", 'i', "xi"},
|
|
{0X307E6E1C, &WrapI_UI<sceVideocodecDelete>, "sceVideocodecDelete", 'i', "xi"},
|
|
{0XA2F0564E, &WrapI_UI<sceVideocodecStop>, "sceVideocodecStop", 'i', "xi"},
|
|
{0X17CF7D2C, &WrapI_UI<sceVideocodecGetFrameCrop>, "sceVideocodecGetFrameCrop", 'i', "xi"},
|
|
{0X26927D19, &WrapI_UI<sceVideocodecGetVersion>, "sceVideocodecGetVersion", 'i', "xi"},
|
|
{0X627B7D42, &WrapI_UI<sceVideocodecGetSEI>, "sceVideocodecGetSEI", 'i', "xi"},
|
|
{0X893B32B1, &WrapI_UI<sceVideocodec_893B32B1>, "sceVideocodec_893B32B1", 'i', "xi"},
|
|
{0XD95C24D5, &WrapI_UI<sceVideocodec_D95C24D5>, "sceVideocodec_D95C24D5", 'i', "xi"},
|
|
};
|
|
|
|
void Register_sceVideocodec() {
|
|
RegisterHLEModule("sceVideocodec", ARRAY_SIZE(sceVideocodec), sceVideocodec);
|
|
}
|