Files
ppsspp/Core/HW/MediaEngine.cpp
T
Henrik RydgårdandClaude Opus 5.5 43f7674f9c SaveState: Check the audio and media modules' values from a state
A review of their savestates found nothing wrong with a normal save and
load, but a broken or crafted state could crash or write out of bounds:

- Atrac2's context pointer was dereferenced and its SAS buffer index used
  unchecked. Atrac v1's file size sized an allocation before it was known
  to be in the file, and its first buffer could be larger than the file.
- MediaEngine's MPEG header size and read position index a fixed array,
  and its ringbuffer size sizes buffers.
- AuCtx's PCM buffer must hold two frames, as the game side requires. And
  AuDecode compared a decoded frame with the whole PCM buffer instead of
  the half it fills, so a frame between the two zero-filled a negative
  size.
- BufferQueue's fill level, VAG block positions (now ending the voice,
  like the other bad VAG state), sceAudiocodec's context count, the SRC
  mix frequency (divided by), and old sceMpeg states' ringbuffer address.
- SAS decoding with an atrac id that has no atrac crashed after warning.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 00:20:24 +02:00

1182 lines
32 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/.
#include "Common/Serialize/SerializeFuncs.h"
#include "Common/Math/SIMDHeaders.h"
#include "Common/StringUtils.h"
#include "Core/System.h"
#include "Core/Debugger/MemBlockInfo.h"
#include "Core/HW/MediaEngine.h"
#include "Core/MemMap.h"
#include "Core/Reporting.h"
#include "GPU/GPUState.h" // Used by TextureDecoder.h when templates get instanced
#include "GPU/Common/TextureDecoder.h"
#include "Core/HW/SimpleAudioDec.h"
#include <algorithm>
#ifdef USE_FFMPEG
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/imgutils.h"
#include "libswscale/swscale.h"
#if LIBAVFORMAT_VERSION_MAJOR >= 59
// private libavformat api (see demux.h in ffmpeg src tree)
void avpriv_stream_set_need_parsing(AVStream *st, enum AVStreamParseType type);
#endif
}
#endif // USE_FFMPEG
#ifdef USE_FFMPEG
#include "Core/FFMPEGCompat.h"
static AVPixelFormat getSwsFormat(int pspFormat)
{
switch (pspFormat)
{
case GE_CMODE_16BIT_BGR5650:
return AV_PIX_FMT_BGR565LE;
case GE_CMODE_16BIT_ABGR5551:
return AV_PIX_FMT_BGR555LE;
case GE_CMODE_16BIT_ABGR4444:
return AV_PIX_FMT_BGR444LE;
case GE_CMODE_32BIT_ABGR8888:
return AV_PIX_FMT_RGBA;
default:
ERROR_LOG(Log::ME, "Unknown pixel format");
return (AVPixelFormat)0;
}
}
void ffmpeg_logger(void *, int level, const char *format, va_list va_args) {
// We're still called even if the level doesn't match.
if (level > av_log_get_level())
return;
char tmp[1024];
vsnprintf(tmp, sizeof(tmp), format, va_args);
tmp[sizeof(tmp) - 1] = '\0';
// Strip off any trailing newline.
size_t len = strlen(tmp);
if (tmp[len - 1] == '\n')
tmp[len - 1] = '\0';
if (!strcmp(tmp, "GHA Phase shifting")) {
Reporting::ReportMessage("Atrac3+: GHA phase shifting");
}
// Let's color the log line appropriately.
if (level <= AV_LOG_PANIC) {
ERROR_LOG(Log::ME, "FF: %s", tmp);
} else if (level >= AV_LOG_VERBOSE) {
DEBUG_LOG(Log::ME, "FF: %s", tmp);
} else {
// Downgrade some log messages we don't care about
if (startsWith(tmp, "No accelerated colorspace") || startsWith(tmp, "SEI type 1 size 40 truncated at 36")) {
VERBOSE_LOG(Log::ME, "FF: %s", tmp);
} else {
INFO_LOG(Log::ME, "FF: %s", tmp);
}
}
}
bool InitFFmpeg() {
#ifdef _DEBUG
av_log_set_level(AV_LOG_VERBOSE);
#else
av_log_set_level(AV_LOG_WARNING);
#endif
av_log_set_callback(&ffmpeg_logger);
return true;
}
#endif
static int getPixelFormatBytes(int pspFormat)
{
switch (pspFormat)
{
case GE_CMODE_16BIT_BGR5650:
case GE_CMODE_16BIT_ABGR5551:
case GE_CMODE_16BIT_ABGR4444:
return 2;
case GE_CMODE_32BIT_ABGR8888:
return 4;
default:
ERROR_LOG(Log::ME, "Unknown pixel format");
return 4;
}
}
MediaEngine::MediaEngine() {
m_bufSize = 0x2000;
m_mpegheaderSize = sizeof(m_mpegheader);
m_audioType = PSP_CODEC_AT3PLUS; // in movie, we use only AT3+ audio
}
MediaEngine::~MediaEngine() {
closeMedia();
}
void MediaEngine::closeMedia() {
closeContext();
delete m_pdata;
delete m_demux;
m_pdata = nullptr;
m_demux = nullptr;
AudioClose(&m_audioContext);
m_isVideoEnd = false;
}
void MediaEngine::DoState(PointerWrap &p) {
auto s = p.Section("MediaEngine", 1, 7);
if (!s)
return;
Do(p, m_videoStream);
Do(p, m_audioStream);
DoArray(p, m_mpegheader, sizeof(m_mpegheader));
if (s >= 4) {
Do(p, m_mpegheaderSize);
} else {
m_mpegheaderSize = sizeof(m_mpegheader);
}
if (s >= 5) {
Do(p, m_mpegheaderReadPos);
} else {
m_mpegheaderReadPos = m_mpegheaderSize;
}
if (s >= 6) {
Do(p, m_expectedVideoStreams);
} else {
m_expectedVideoStreams = 0;
}
Do(p, m_ringbuffersize);
// All straight from the file. The header ones index m_mpegheader, the ringbuffer size sizes
// buffers (the PSP's whole RAM is a generous limit.)
if (p.mode == p.MODE_READ && (m_mpegheaderSize < 0 || m_mpegheaderSize > (int)sizeof(m_mpegheader) ||
m_mpegheaderReadPos < 0 || m_mpegheaderReadPos > m_mpegheaderSize || m_ringbuffersize < 0 || m_ringbuffersize > 64 * 1024 * 1024)) {
ERROR_LOG(Log::SaveState, "MediaEngine: bad header size %d, position %d or ringbuffer size %d in savestate", m_mpegheaderSize, m_mpegheaderReadPos, m_ringbuffersize);
p.SetError(p.ERROR_FAILURE);
return;
}
u32 hasloadStream = m_pdata != nullptr;
Do(p, hasloadStream);
if (hasloadStream && p.mode == p.MODE_READ)
reloadStream();
#ifdef USE_FFMPEG
u32 hasopencontext = m_pFormatCtx != nullptr;
#else
u32 hasopencontext = false;
#endif
Do(p, hasopencontext);
if (m_pdata)
m_pdata->DoState(p);
if (m_demux)
m_demux->DoState(p);
Do(p, m_videopts);
if (s >= 7) {
Do(p, m_lastPts);
} else {
m_lastPts = m_videopts;
}
Do(p, m_audiopts);
if (s >= 2) {
Do(p, m_firstTimeStamp);
Do(p, m_lastTimeStamp);
}
if (hasopencontext && p.mode == p.MODE_READ) {
openContext(true);
}
Do(p, m_isVideoEnd);
bool noAudioDataRemoved;
Do(p, noAudioDataRemoved);
if (s >= 3) {
Do(p, m_audioType);
} else {
m_audioType = PSP_CODEC_AT3PLUS;
}
}
int MediaEngine::MpegReadbuffer(void *opaque, uint8_t *buf, int buf_size) {
MediaEngine *mpeg = (MediaEngine *)opaque;
int size = buf_size;
if (mpeg->m_mpegheaderReadPos < mpeg->m_mpegheaderSize) {
size = std::min(buf_size, mpeg->m_mpegheaderSize - mpeg->m_mpegheaderReadPos);
memcpy(buf, mpeg->m_mpegheader + mpeg->m_mpegheaderReadPos, size);
mpeg->m_mpegheaderReadPos += size;
} else {
size = mpeg->m_pdata->pop_front(buf, buf_size);
if (size > 0)
mpeg->m_decodingsize = size;
}
return size;
}
bool MediaEngine::SetupStreams() {
#ifdef USE_FFMPEG
const u32 magic = *(u32_le *)&m_mpegheader[0];
if (magic != PSMF_MAGIC) {
WARN_LOG_REPORT(Log::ME, "Could not setup streams, bad magic: %08x", magic);
return false;
}
int numStreams = *(u16_be *)&m_mpegheader[0x80];
if (numStreams <= 0 || numStreams > 8) {
// Looks crazy. Let's bail out and let FFmpeg handle it.
WARN_LOG_REPORT(Log::ME, "Could not setup streams, unexpected stream count: %d", numStreams);
return false;
}
// Looking good. Let's add those streams.
int videoStreamNum = -1;
for (int i = 0; i < numStreams; i++) {
const u8 *const currentStreamAddr = m_mpegheader + 0x82 + i * 16;
int streamId = currentStreamAddr[0];
// We only set video streams. We demux the audio stream separately.
if ((streamId & PSMF_VIDEO_STREAM_ID) == PSMF_VIDEO_STREAM_ID) {
++videoStreamNum;
addVideoStream(videoStreamNum, streamId);
}
}
// Add the streams to meet the expectation.
for (int i = videoStreamNum + 1; i < m_expectedVideoStreams; i++) {
addVideoStream(i);
}
#endif
return true;
}
bool MediaEngine::openContext(bool keepReadPos) {
#ifdef USE_FFMPEG
InitFFmpeg();
if (m_pFormatCtx || !m_pdata)
return false;
if (!keepReadPos) {
m_mpegheaderReadPos = 0;
}
m_decodingsize = 0;
m_bufSize = std::max(m_bufSize, m_mpegheaderSize);
u8 *tempbuf = (u8*)av_malloc(m_bufSize);
m_pFormatCtx = avformat_alloc_context();
m_pIOContext = avio_alloc_context(tempbuf, m_bufSize, 0, (void*)this, &MpegReadbuffer, nullptr, nullptr);
m_pFormatCtx->pb = m_pIOContext;
// Open video file
AVDictionary *open_opt = nullptr;
av_dict_set_int(&open_opt, "probesize", m_mpegheaderSize, 0);
if (avformat_open_input((AVFormatContext**)&m_pFormatCtx, nullptr, nullptr, &open_opt) != 0) {
av_dict_free(&open_opt);
return false;
}
av_dict_free(&open_opt);
bool usedFFMPEGFindStreamInfo = false;
if (!SetupStreams() || PSP_CoreParameter().compat.flags().UseFFMPEGFindStreamInfo) {
// Fallback to old behavior. Reads too much and corrupts when game doesn't read fast enough.
// SetupStreams sometimes work for newer FFmpeg 3.1+ now, but sometimes framerate is missing.
WARN_LOG_REPORT_ONCE(setupStreams, Log::ME, "Failed to read valid video stream data from header");
if (avformat_find_stream_info(m_pFormatCtx, nullptr) < 0) {
closeContext();
return false;
}
usedFFMPEGFindStreamInfo = true;
}
if (m_videoStream >= (int)m_pFormatCtx->nb_streams) {
WARN_LOG_REPORT(Log::ME, "Bad video stream %d", m_videoStream);
m_videoStream = -1;
}
if (m_videoStream == -1) {
// Find the first video stream
for (int i = 0; i < (int)m_pFormatCtx->nb_streams; i++) {
const AVStream *s = m_pFormatCtx->streams[i];
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)
AVMediaType type = s->codecpar->codec_type;
#else
AVMediaType type = s->codec->codec_type;
#endif
if (type == AVMEDIA_TYPE_VIDEO) {
m_videoStream = i;
break;
}
}
if (m_videoStream == -1)
return false;
}
// Here it shouldn't be possible for m_videoStream to be invalid.
if (!setVideoStream(m_videoStream, true))
return false;
setVideoDim();
m_audioContext = CreateAudioDecoder((PSPAudioType)m_audioType);
m_isVideoEnd = false;
if (PSP_CoreParameter().compat.flags().UseFFMPEGFindStreamInfo && usedFFMPEGFindStreamInfo) {
m_mpegheaderReadPos++;
av_seek_frame(m_pFormatCtx, m_videoStream, 0, 0);
}
#endif // USE_FFMPEG
return true;
}
void MediaEngine::closeContext() {
#ifdef USE_FFMPEG
if (m_buffer)
av_free(m_buffer);
if (m_pFrameRGB)
av_frame_free(&m_pFrameRGB);
if (m_pFrame)
av_frame_free(&m_pFrame);
if (m_pIOContext && m_pIOContext->buffer)
av_free(m_pIOContext->buffer);
if (m_pIOContext)
av_free(m_pIOContext);
for (auto &it : m_pCodecCtxs) {
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)
avcodec_free_context(&it.second);
#else
avcodec_close(it.second);
#endif
}
m_pCodecCtxs.clear();
// These are streams allocated from avformat_new_stream.
for (auto &it : m_codecsToClose) {
#if LIBAVCODEC_VERSION_MAJOR >= 62
avcodec_free_context(&it);
#else
avcodec_close(it);
#endif
}
m_codecsToClose.clear();
if (m_pFormatCtx)
avformat_close_input(&m_pFormatCtx);
sws_freeContext(m_sws_ctx);
m_sws_ctx = nullptr;
m_pIOContext = nullptr;
#endif
m_buffer = nullptr;
}
bool MediaEngine::loadStream(const u8 *buffer, int readSize, int RingbufferSize)
{
closeMedia();
m_videopts = 0;
m_lastPts = -1;
m_audiopts = 0;
m_ringbuffersize = RingbufferSize;
m_pdata = new BufferQueue(RingbufferSize + 2048);
m_pdata->push(buffer, readSize);
m_firstTimeStamp = getMpegTimeStamp(buffer + PSMF_FIRST_TIMESTAMP_OFFSET);
m_lastTimeStamp = getMpegTimeStamp(buffer + PSMF_LAST_TIMESTAMP_OFFSET);
int mpegoffset = (int)(*(s32_be*)(buffer + 8));
m_demux = new MpegDemux(RingbufferSize + 2048, mpegoffset);
m_demux->addStreamData(buffer, readSize);
return true;
}
bool MediaEngine::reloadStream()
{
return loadStream(m_mpegheader, 2048, m_ringbuffersize);
}
bool MediaEngine::addVideoStream(int streamNum, int streamId) {
#ifdef USE_FFMPEG
if (m_pFormatCtx) {
// no need to add an existing stream.
if ((u32)streamNum < m_pFormatCtx->nb_streams)
return true;
AVCodec *h264_codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!h264_codec)
return false;
AVStream *stream = avformat_new_stream(m_pFormatCtx, h264_codec);
if (stream) {
// Reference ISO/IEC 13818-1.
if (streamId == -1)
streamId = PSMF_VIDEO_STREAM_ID | streamNum;
stream->id = 0x00000100 | streamId;
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
stream->codecpar->codec_id = AV_CODEC_ID_H264;
#endif
#if LIBAVFORMAT_VERSION_MAJOR >= 59
avpriv_stream_set_need_parsing(stream, AVSTREAM_PARSE_FULL);
#else
stream->request_probe = 0;
stream->need_parsing = AVSTREAM_PARSE_FULL;
#endif
// We could set the width here, but we don't need to.
if (streamNum >= m_expectedVideoStreams) {
++m_expectedVideoStreams;
}
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(59, 16, 100)
AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
AVCodecContext *codecCtx = avcodec_alloc_context3(codec);
#else
AVCodecContext *codecCtx = stream->codec;
#endif
m_codecsToClose.push_back(codecCtx);
return true;
}
}
#endif
if (streamNum >= m_expectedVideoStreams) {
++m_expectedVideoStreams;
}
return false;
}
int MediaEngine::addStreamData(const u8 *buffer, int addSize) {
int size = addSize;
if (size > 0 && m_pdata) {
if (!m_pdata->push(buffer, size))
size = 0;
if (m_demux) {
m_demux->addStreamData(buffer, addSize);
}
#ifdef USE_FFMPEG
if (!m_pFormatCtx && m_pdata->getQueueSize() >= 2048) {
m_mpegheaderSize = m_pdata->get_front(m_mpegheader, sizeof(m_mpegheader));
int streamOffset = (int)(*(s32_be *)(m_mpegheader + 8));
if (streamOffset <= m_mpegheaderSize) {
m_mpegheaderSize = streamOffset;
m_pdata->pop_front(0, m_mpegheaderSize);
openContext();
}
}
#endif // USE_FFMPEG
// We added data, so... not the end anymore?
m_isVideoEnd = false;
}
return size;
}
bool MediaEngine::seekTo(s64 timestamp, int videoPixelMode) {
if (timestamp <= 0) {
return true;
}
// Just doing it the not so great way to be sure audio is in sync.
int timeout = 1000;
while (getVideoTimeStamp() < timestamp - 3003) {
if (getAudioTimeStamp() < getVideoTimeStamp() - 4180 * 2) {
getNextAudioFrame(NULL, NULL, NULL);
}
if (!stepVideo(videoPixelMode, true)) {
return false;
}
if (--timeout <= 0) {
return true;
}
}
while (getAudioTimeStamp() < getVideoTimeStamp() - 4180 * 2) {
if (getNextAudioFrame(NULL, NULL, NULL) == 0) {
return false;
}
if (--timeout <= 0) {
return true;
}
}
return true;
}
bool MediaEngine::setVideoStream(int streamNum, bool force) {
if (m_videoStream == streamNum && !force) {
// Yay, nothing to do.
return true;
}
#ifdef USE_FFMPEG
if (m_pFormatCtx && m_pCodecCtxs.find(streamNum) == m_pCodecCtxs.end()) {
// Get a pointer to the codec context for the video stream
if ((u32)streamNum >= m_pFormatCtx->nb_streams) {
return false;
}
AVStream *stream = m_pFormatCtx->streams[streamNum];
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)
AVCodec *pCodec = avcodec_find_decoder(stream->codecpar->codec_id);
if (!pCodec) {
WARN_LOG_REPORT(Log::ME, "Could not find decoder for %d", (int)stream->codecpar->codec_id);
return false;
}
AVCodecContext *m_pCodecCtx = avcodec_alloc_context3(pCodec);
int paramResult = avcodec_parameters_to_context(m_pCodecCtx, stream->codecpar);
if (paramResult < 0) {
WARN_LOG_REPORT(Log::ME, "Failed to prepare context parameters: %08x", paramResult);
return false;
}
#else
AVCodecContext *m_pCodecCtx = stream->codec;
// Find the decoder for the video stream
AVCodec *pCodec = avcodec_find_decoder(m_pCodecCtx->codec_id);
if (pCodec == nullptr) {
return false;
}
#endif
m_pCodecCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT | AV_CODEC_FLAG_LOW_DELAY;
AVDictionary *opt = nullptr;
// Allow ffmpeg to use any number of threads it wants. Without this, it doesn't use threads.
av_dict_set(&opt, "threads", "0", 0);
int openResult = avcodec_open2(m_pCodecCtx, pCodec, &opt);
av_dict_free(&opt);
if (openResult < 0) {
return false;
}
m_pCodecCtxs[streamNum] = m_pCodecCtx;
}
#endif
m_videoStream = streamNum;
return true;
}
bool MediaEngine::setAudioStream(int streamNum) {
if (m_audioStream == streamNum)
return true;
m_audioStream = streamNum;
if (m_demux) {
m_demux->setAudioChannel(streamNum);
}
return true;
}
bool MediaEngine::setVideoDim(int width, int height)
{
#ifdef USE_FFMPEG
auto codecIter = m_pCodecCtxs.find(m_videoStream);
if (codecIter == m_pCodecCtxs.end())
return false;
AVCodecContext *m_pCodecCtx = codecIter->second;
if (width == 0 && height == 0)
{
// use the orignal video size
m_desWidth = m_pCodecCtx->width;
m_desHeight = m_pCodecCtx->height;
}
else
{
m_desWidth = width;
m_desHeight = height;
}
// Allocate video frame
if (!m_pFrame) {
m_pFrame = av_frame_alloc();
}
sws_freeContext(m_sws_ctx);
m_sws_ctx = nullptr;
m_sws_fmt = -1;
if (m_desWidth == 0 || m_desHeight == 0) {
// Can't setup SWS yet, so stop for now.
return false;
}
updateSwsFormat(GE_CMODE_32BIT_ABGR8888);
// Allocate video frame for RGB24
m_pFrameRGB = av_frame_alloc();
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 12, 100)
int numBytes = av_image_get_buffer_size((AVPixelFormat)m_sws_fmt, m_desWidth, m_desHeight, 1);
#else
int numBytes = avpicture_get_size((AVPixelFormat)m_sws_fmt, m_desWidth, m_desHeight);
#endif
m_buffer = (u8*)av_malloc(numBytes * sizeof(uint8_t));
// Assign appropriate parts of buffer to image planes in m_pFrameRGB
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 12, 100)
av_image_fill_arrays(m_pFrameRGB->data, m_pFrameRGB->linesize, m_buffer, (AVPixelFormat)m_sws_fmt, m_desWidth, m_desHeight, 1);
#else
avpicture_fill((AVPicture *)m_pFrameRGB, m_buffer, (AVPixelFormat)m_sws_fmt, m_desWidth, m_desHeight);
#endif
#endif // USE_FFMPEG
return true;
}
void MediaEngine::updateSwsFormat(int videoPixelMode) {
#ifdef USE_FFMPEG
auto codecIter = m_pCodecCtxs.find(m_videoStream);
AVCodecContext *m_pCodecCtx = codecIter == m_pCodecCtxs.end() ? 0 : codecIter->second;
AVPixelFormat swsDesired = getSwsFormat(videoPixelMode);
if (swsDesired != m_sws_fmt && m_pCodecCtx != 0) {
m_sws_fmt = swsDesired;
m_sws_ctx = sws_getCachedContext
(
m_sws_ctx,
m_pCodecCtx->width,
m_pCodecCtx->height,
m_pCodecCtx->pix_fmt,
m_desWidth,
m_desHeight,
(AVPixelFormat)m_sws_fmt,
SWS_BILINEAR,
NULL,
NULL,
NULL
);
int *inv_coefficients;
int *coefficients;
int srcRange, dstRange;
int brightness, contrast, saturation;
if (sws_getColorspaceDetails(m_sws_ctx, &inv_coefficients, &srcRange, &coefficients, &dstRange, &brightness, &contrast, &saturation) != -1) {
srcRange = 0;
dstRange = 0;
sws_setColorspaceDetails(m_sws_ctx, inv_coefficients, srcRange, coefficients, dstRange, brightness, contrast, saturation);
}
}
#endif
}
bool MediaEngine::stepVideo(int videoPixelMode, bool skipFrame) {
#ifdef USE_FFMPEG
auto codecIter = m_pCodecCtxs.find(m_videoStream);
AVCodecContext *m_pCodecCtx = codecIter == m_pCodecCtxs.end() ? 0 : codecIter->second;
if (!m_pFormatCtx)
return false;
if (!m_pCodecCtx)
return false;
if (!m_pFrame)
return false;
AVPacket packet;
av_init_packet(&packet);
int frameFinished;
bool bGetFrame = false;
while (!bGetFrame) {
bool dataEnd = av_read_frame(m_pFormatCtx, &packet) < 0;
// Even if we've read all frames, some may have been re-ordered frames at the end.
// Still need to decode those, so keep calling avcodec_decode_video2() / avcodec_receive_frame().
if (dataEnd || packet.stream_index == m_videoStream) {
// avcodec_decode_video2() / avcodec_send_packet() gives us the re-ordered frames with a NULL packet.
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 12, 100)
if (dataEnd)
av_packet_unref(&packet);
#else
if (dataEnd)
av_free_packet(&packet);
#endif
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 48, 101)
if (packet.size != 0)
avcodec_send_packet(m_pCodecCtx, &packet);
int result = avcodec_receive_frame(m_pCodecCtx, m_pFrame);
if (result == 0) {
result = 1;
frameFinished = 1;
} else if (result == AVERROR(EAGAIN)) {
result = 0;
frameFinished = 0;
} else {
frameFinished = 0;
}
#else
int result = avcodec_decode_video2(m_pCodecCtx, m_pFrame, &frameFinished, &packet);
#endif
if (frameFinished) {
if (!m_pFrameRGB) {
setVideoDim();
}
if (m_pFrameRGB && !skipFrame) {
updateSwsFormat(videoPixelMode);
// TODO: Technically we could set this to frameWidth instead of m_desWidth for better perf.
// Update the linesize for the new format too. We started with the largest size, so it should fit.
m_pFrameRGB->linesize[0] = getPixelFormatBytes(videoPixelMode) * m_desWidth;
sws_scale(m_sws_ctx, m_pFrame->data, m_pFrame->linesize, 0,
m_pCodecCtx->height, m_pFrameRGB->data, m_pFrameRGB->linesize);
}
#if LIBAVUTIL_VERSION_MAJOR >= 59
int64_t bestPts = m_pFrame->best_effort_timestamp;
int64_t ptsDuration = m_pFrame->duration;
#elif LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(55, 58, 100)
int64_t bestPts = m_pFrame->best_effort_timestamp;
int64_t ptsDuration = m_pFrame->pkt_duration;
#else
int64_t bestPts = av_frame_get_best_effort_timestamp(m_pFrame);
int64_t ptsDuration = av_frame_get_pkt_duration(m_pFrame);
#endif
if (ptsDuration == 0) {
if (m_lastPts == bestPts - m_firstTimeStamp || bestPts == AV_NOPTS_VALUE) {
// TODO: Assuming 29.97 if missing.
m_videopts += 3003;
} else {
m_videopts = bestPts - m_firstTimeStamp;
m_lastPts = m_videopts;
}
} else if (bestPts != AV_NOPTS_VALUE) {
m_videopts = bestPts + ptsDuration - m_firstTimeStamp;
m_lastPts = m_videopts;
} else {
m_videopts += ptsDuration;
m_lastPts = m_videopts;
}
bGetFrame = true;
}
if (result <= 0 && dataEnd) {
// Sometimes, m_readSize is less than m_streamSize at the end, but not by much.
// This is kinda a hack, but the ringbuffer would have to be prematurely empty too.
m_isVideoEnd = !bGetFrame && (m_pdata->getQueueSize() == 0);
if (m_isVideoEnd)
m_decodingsize = 0;
break;
}
}
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 12, 100)
av_packet_unref(&packet);
#else
av_free_packet(&packet);
#endif
}
return bGetFrame;
#else
// If video engine is not available, just add to the timestamp at least.
m_videopts += 3003;
return true;
#endif // USE_FFMPEG
}
// Helpers that null out alpha (which seems to be the case on the PSP.)
// Some games depend on this, for example Sword Art Online (doesn't clear A's from buffer.)
inline void writeVideoLineRGBA(void *destp, const void *srcp, int width) {
// TODO: Use SSE/NEON, investigate why AV_PIX_FMT_RGB0 does not work.
u32_le *dest = (u32_le *)destp;
const u32_le *src = (u32_le *)srcp;
int count = width;
#if PPSSPP_ARCH(SSE2)
__m128i mask = _mm_set1_epi32(0x00FFFFFF);
while (count >= 8) {
__m128i pixels1 = _mm_and_si128(_mm_loadu_si128((const __m128i *)src), mask);
__m128i pixels2 = _mm_and_si128(_mm_loadu_si128((const __m128i *)src + 1), mask);
_mm_storeu_si128((__m128i *)dest, pixels1);
_mm_storeu_si128((__m128i *)dest + 1, pixels2);
src += 8;
dest += 8;
count -= 8;
}
#elif PPSSPP_ARCH(ARM_NEON)
uint32x4_t mask = vdupq_n_u32(0x00FFFFFF);
while (count >= 8) {
uint32x4_t pixels1 = vandq_u32(vld1q_u32(src), mask);
uint32x4_t pixels2 = vandq_u32(vld1q_u32(src + 4), mask);
vst1q_u32(dest, pixels1);
vst1q_u32(dest + 4, pixels2);
src += 8;
dest += 8;
count -= 8;
}
#endif
const u32 mask32 = 0x00FFFFFF;
DO_NOT_VECTORIZE_LOOP
while (count--) {
*dest++ = *src++ & mask32;
}
}
inline void writeVideoLineABGR5650(void *destp, const void *srcp, int width) {
memcpy(destp, srcp, width * sizeof(u16));
}
inline void writeVideoLineABGR5551(void *destp, const void *srcp, int width) {
// TODO: Use SSE/NEON.
u16_le *dest = (u16_le *)destp;
const u16_le *src = (u16_le *)srcp;
const u16 mask = 0x7FFF;
for (int i = 0; i < width; ++i) {
dest[i] = src[i] & mask;
}
}
inline void writeVideoLineABGR4444(void *destp, const void *srcp, int width) {
// TODO: Use SSE/NEON.
u16_le *dest = (u16_le *)destp;
const u16_le *src = (u16_le *)srcp;
const u16 mask = 0x0FFF;
for (int i = 0; i < width; ++i) {
dest[i] = src[i] & mask;
}
}
// Returns -1 if the size is unreasonable. Swizzling writes whole 8-row tiles, at least one.
static int VideoImageSize(int videoLineSize, int height, bool swizzle) {
if (videoLineSize < 0 || height < 0) {
return -1;
}
if (swizzle) {
height = std::max(8, (height + 7) & ~7);
}
const s64 size = (s64)videoLineSize * height;
return size > 0x10000000 ? -1 : (int)size;
}
int MediaEngine::writeVideoImage(u32 bufferPtr, int frameWidth, int videoPixelMode) {
int videoLineSize = 0;
switch (videoPixelMode) {
case GE_CMODE_32BIT_ABGR8888:
videoLineSize = frameWidth * sizeof(u32);
break;
case GE_CMODE_16BIT_BGR5650:
case GE_CMODE_16BIT_ABGR5551:
case GE_CMODE_16BIT_ABGR4444:
videoLineSize = frameWidth * sizeof(u16);
break;
}
const int height = m_desHeight;
const bool swizzle = Memory::IsVRAMAddress(bufferPtr) && (bufferPtr & 0x00200000) == 0x00200000;
const int videoImageSize = VideoImageSize(videoLineSize, height, swizzle);
if (frameWidth <= 0 || frameWidth > 2048 || videoImageSize < 0 || !Memory::IsValidRange(bufferPtr, videoImageSize)) {
// Clearly invalid values. Let's just not.
ERROR_LOG_REPORT(Log::ME, "Ignoring invalid video decode address %08x/%x", bufferPtr, frameWidth);
return 0;
}
u8 *buffer = Memory::GetPointerWriteUnchecked(bufferPtr);
#ifdef USE_FFMPEG
if (!m_pFrame || !m_pFrameRGB)
return 0;
// lock the image size
// Rows can't be wider than the stride, or they'd overlap and the last would run past the end.
const int width = std::min(m_desWidth, frameWidth);
const int srcStride = m_desWidth;
u8 *imgbuf = buffer;
const u8 *data = m_pFrameRGB->data[0];
if (swizzle) {
// Zeroed, since the padding past width (and the rows of the last tile) gets copied to the guest too.
imgbuf = new u8[videoImageSize]();
}
switch (videoPixelMode) {
case GE_CMODE_32BIT_ABGR8888:
for (int y = 0; y < height; y++) {
writeVideoLineRGBA(imgbuf + videoLineSize * y, data, width);
data += srcStride * sizeof(u32);
}
break;
case GE_CMODE_16BIT_BGR5650:
for (int y = 0; y < height; y++) {
writeVideoLineABGR5650(imgbuf + videoLineSize * y, data, width);
data += srcStride * sizeof(u16);
}
break;
case GE_CMODE_16BIT_ABGR5551:
for (int y = 0; y < height; y++) {
writeVideoLineABGR5551(imgbuf + videoLineSize * y, data, width);
data += srcStride * sizeof(u16);
}
break;
case GE_CMODE_16BIT_ABGR4444:
for (int y = 0; y < height; y++) {
writeVideoLineABGR4444(imgbuf + videoLineSize * y, data, width);
data += srcStride * sizeof(u16);
}
break;
default:
ERROR_LOG_REPORT(Log::ME, "Unsupported video pixel format %d", videoPixelMode);
break;
}
if (swizzle) {
const int bxc = videoLineSize / 16;
int byc = (height + 7) / 8;
if (byc == 0)
byc = 1;
DoSwizzleTex16((const u32 *)imgbuf, buffer, bxc, byc, videoLineSize);
delete [] imgbuf;
}
NotifyMemInfo(MemBlockFlags::WRITE, bufferPtr, videoImageSize, "VideoDecode");
return videoImageSize;
#endif // USE_FFMPEG
return 0;
}
int MediaEngine::writeVideoImageWithRange(u32 bufferPtr, int frameWidth, int videoPixelMode,
int xpos, int ypos, int width, int height) {
int videoLineSize = 0;
switch (videoPixelMode) {
case GE_CMODE_32BIT_ABGR8888:
videoLineSize = frameWidth * sizeof(u32);
break;
case GE_CMODE_16BIT_BGR5650:
case GE_CMODE_16BIT_ABGR5551:
case GE_CMODE_16BIT_ABGR4444:
videoLineSize = frameWidth * sizeof(u16);
break;
}
const bool swizzle = Memory::IsVRAMAddress(bufferPtr) && (bufferPtr & 0x00200000) == 0x00200000;
const int videoImageSize = VideoImageSize(videoLineSize, height, swizzle);
if (frameWidth <= 0 || frameWidth > 2048 || videoImageSize < 0 || !Memory::IsValidRange(bufferPtr, videoImageSize)) {
// Clearly invalid values. Let's just not.
ERROR_LOG_REPORT(Log::ME, "Ignoring invalid video decode address %08x/%x", bufferPtr, frameWidth);
return 0;
}
u8 *buffer = Memory::GetPointerWriteUnchecked(bufferPtr);
#ifdef USE_FFMPEG
if (!m_pFrame || !m_pFrameRGB)
return 0;
// lock the image size
// imgbuf advances per row, so keep the start for the swizzle.
u8 *const swizzleBuf = swizzle ? new u8[videoImageSize]() : nullptr;
u8 *imgbuf = swizzle ? swizzleBuf : buffer;
const u8 *data = m_pFrameRGB->data[0];
if (width > m_desWidth - xpos)
width = m_desWidth - xpos;
if (width > frameWidth)
width = frameWidth;
if (height > m_desHeight - ypos)
height = m_desHeight - ypos;
switch (videoPixelMode) {
case GE_CMODE_32BIT_ABGR8888:
data += (ypos * m_desWidth + xpos) * sizeof(u32);
for (int y = 0; y < height; y++) {
writeVideoLineRGBA(imgbuf, data, width);
data += m_desWidth * sizeof(u32);
imgbuf += videoLineSize;
}
break;
case GE_CMODE_16BIT_BGR5650:
data += (ypos * m_desWidth + xpos) * sizeof(u16);
for (int y = 0; y < height; y++) {
writeVideoLineABGR5650(imgbuf, data, width);
data += m_desWidth * sizeof(u16);
imgbuf += videoLineSize;
}
break;
case GE_CMODE_16BIT_ABGR5551:
data += (ypos * m_desWidth + xpos) * sizeof(u16);
for (int y = 0; y < height; y++) {
writeVideoLineABGR5551(imgbuf, data, width);
data += m_desWidth * sizeof(u16);
imgbuf += videoLineSize;
}
break;
case GE_CMODE_16BIT_ABGR4444:
data += (ypos * m_desWidth + xpos) * sizeof(u16);
for (int y = 0; y < height; y++) {
writeVideoLineABGR4444(imgbuf, data, width);
data += m_desWidth * sizeof(u16);
imgbuf += videoLineSize;
}
break;
default:
ERROR_LOG_REPORT(Log::ME, "Unsupported video pixel format %d", videoPixelMode);
break;
}
if (swizzle) {
WARN_LOG_REPORT_ONCE(vidswizzle, Log::ME, "Swizzling Video with range");
const int bxc = videoLineSize / 16;
int byc = (height + 7) / 8;
if (byc == 0)
byc = 1;
DoSwizzleTex16((const u32 *)swizzleBuf, buffer, bxc, byc, videoLineSize);
delete [] swizzleBuf;
}
NotifyMemInfo(MemBlockFlags::WRITE, bufferPtr, videoImageSize, "VideoDecodeRange");
return videoImageSize;
#endif // USE_FFMPEG
return 0;
}
u8 *MediaEngine::getFrameImage() {
#ifdef USE_FFMPEG
return m_pFrameRGB->data[0];
#else
return nullptr;
#endif
}
int MediaEngine::getRemainSize() {
if (!m_pdata)
return 0;
return std::max(m_pdata->getRemainSize() - m_decodingsize - 2048, 0);
}
int MediaEngine::getAudioRemainSize() {
if (!m_demux) {
// No audio, so it can't be full, return video instead.
return getRemainSize();
}
return m_demux->getRemainSize();
}
int MediaEngine::getNextAudioFrame(u8 **buf, int *headerCode1, int *headerCode2) {
// When getting a frame, increment pts
m_audiopts += 4180;
// Demux now (rather than on add data) so that we select the right stream.
m_demux->demux(m_audioStream);
s64 pts = 0;
int result = m_demux->getNextAudioFrame(buf, headerCode1, headerCode2, &pts);
if (pts != 0) {
// m_audiopts is supposed to be after the returned frame.
m_audiopts = pts - m_firstTimeStamp + 4180;
}
return result;
}
int MediaEngine::getAudioSamples(u32 bufferPtr) {
int16_t *buffer = (int16_t *)Memory::GetPointerWriteRangeOrException(bufferPtr, 8192);
if (buffer == nullptr) {
ERROR_LOG_REPORT(Log::ME, "Ignoring bad audio decode address %08x during video playback", bufferPtr);
}
if (!m_demux) {
return 0;
}
u8 *audioFrame = nullptr;
int headerCode1, headerCode2;
int frameSize = getNextAudioFrame(&audioFrame, &headerCode1, &headerCode2);
if (frameSize == 0) {
return 0;
}
int outSamples = 0;
if (m_audioContext != nullptr) {
if (headerCode1 == 0x24) {
// This means mono audio - tell the decoder to expect it before the first frame.
// Note that it will always send us back stereo audio.
m_audioContext->SetChannels(1);
}
int inbytesConsumed = 0;
if (!m_audioContext->Decode(audioFrame, frameSize, &inbytesConsumed, 2, buffer, &outSamples)) {
ERROR_LOG(Log::ME, "Audio (%s) decode failed during video playback", GetCodecName(m_audioType));
}
int outBytes = outSamples * sizeof(int16_t) * 2;
NotifyMemInfo(MemBlockFlags::WRITE, bufferPtr, outBytes, "VideoDecodeAudio");
}
return 0x2000;
}
bool MediaEngine::IsNoAudioData() {
if (!m_demux) {
return true;
}
// Let's double check. Here should be a safe enough place to demux.
m_demux->demux(m_audioStream);
return !m_demux->hasNextAudioFrame(NULL, NULL, NULL, NULL);
}
bool MediaEngine::IsActuallyPlayingAudio() {
return getAudioTimeStamp() >= 0;
}
s64 MediaEngine::getVideoTimeStamp() {
return m_videopts;
}
s64 MediaEngine::getAudioTimeStamp() {
return m_demux ? m_audiopts - 4180 : -1;
}
s64 MediaEngine::getLastTimeStamp() {
if (!m_pdata)
return 0;
return m_lastTimeStamp - m_firstTimeStamp;
}
std::string GetFFMPEGVersion() {
#ifdef USE_FFMPEG
std::string comment = "";
if (LIBAVFORMAT_VERSION_MAJOR == 57 && LIBAVFORMAT_VERSION_MINOR == 25 && LIBAVFORMAT_VERSION_MICRO == 100) {
// This is the bundled version.
comment = " (bundled)";
} else {
comment = " (system)";
}
return StringFromFormat("%d.%d.%d (%s)%s",
LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO, av_version_info(), comment.c_str());
#else
return "N/A";
#endif
}