Add AvcDecoder, a plain access-unit-in, frame-out H.264 decoder

sceVideocodec is the interface the Media Engine really exposes: the caller owns
the buffers and hands over one access unit at a time. MediaEngine can't serve
that shape - it is built around sceMpeg's own model, with the PSMF demuxer, the
ringbuffer and its own frame pacing wrapped around the decoder - so this is a
second, separate path rather than a refactor of code every game that plays
video depends on. Merging the two is worth doing once sceVideocodec has earned
its keep.

Nothing calls it yet; sceVideocodec is the next piece.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5 authored and Henrik Rydgård committed 2026-09-12 12:53:13 -06:00
1 parent 7c6f9d18b8
commit 3276cdf65f
9 files changed
+276

No files matched your search

+2
View File
@@ -597,6 +597,8 @@ add_library(Core STATIC
HW/Display.h
HW/GpioMMIO.cpp
HW/GpioMMIO.h
HW/AvcDecoder.cpp
HW/AvcDecoder.h
HW/MediaEngine.cpp
HW/MediaEngine.h
HW/MpegDemux.cpp
+2
View File
@@ -672,6 +672,7 @@
<ClCompile Include="HLE\sceVaudio.cpp" />
<ClCompile Include="HLE\sceVshBridge.cpp" />
<ClCompile Include="HLE\__sceAudio.cpp" />
<ClCompile Include="HW\AvcDecoder.cpp" />
<ClCompile Include="HW\MediaEngine.cpp" />
<ClCompile Include="HW\MemoryStick.cpp" />
<ClCompile Include="HW\MpegDemux.cpp" />
@@ -1189,6 +1190,7 @@
<ClInclude Include="HLE\ThreadQueueList.h" />
<ClInclude Include="HLE\__sceAudio.h" />
<ClInclude Include="HW\BufferQueue.h" />
<ClInclude Include="HW\AvcDecoder.h" />
<ClInclude Include="HW\MediaEngine.h" />
<ClInclude Include="HW\MpegDemux.h" />
<ClInclude Include="HW\SasAudio.h" />
+6
View File
@@ -357,6 +357,9 @@
<ClCompile Include="HLE\sceImpose.cpp">
<Filter>HLE\Libraries</Filter>
</ClCompile>
<ClCompile Include="HW\AvcDecoder.cpp">
<Filter>HW</Filter>
</ClCompile>
<ClCompile Include="HW\MediaEngine.cpp">
<Filter>HW</Filter>
</ClCompile>
@@ -1692,6 +1695,9 @@
<ClInclude Include="HLE\sceImpose.h">
<Filter>HLE\Libraries</Filter>
</ClInclude>
<ClInclude Include="HW\AvcDecoder.h">
<Filter>HW</Filter>
</ClInclude>
<ClInclude Include="HW\MediaEngine.h">
<Filter>HW</Filter>
</ClInclude>
+187
View File
@@ -0,0 +1,187 @@
// 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/.
#include "ppsspp_config.h"
#include "Common/Log.h"
#include "Core/HW/AvcDecoder.h"
#ifdef USE_FFMPEG
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavutil/imgutils.h"
}
#include "Core/FFMPEGCompat.h"
#endif // USE_FFMPEG
AvcDecoder::AvcDecoder() {
#ifdef USE_FFMPEG
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
ERROR_LOG(Log::ME, "AvcDecoder: no H.264 decoder in this ffmpeg build");
return;
}
codecCtx_ = avcodec_alloc_context3(codec);
if (!codecCtx_) {
ERROR_LOG(Log::ME, "AvcDecoder: couldn't allocate a codec context");
return;
}
// The PSP hands us whole access units, so no parser is needed and no truncation is expected.
codecCtx_->flags = 0;
codecCtx_->flags2 = 0;
if (avcodec_open2(codecCtx_, codec, nullptr) < 0) {
ERROR_LOG(Log::ME, "AvcDecoder: couldn't open the H.264 decoder");
avcodec_free_context(&codecCtx_);
codecCtx_ = nullptr;
return;
}
frame_ = av_frame_alloc();
if (!frame_) {
avcodec_free_context(&codecCtx_);
codecCtx_ = nullptr;
}
#endif
}
AvcDecoder::~AvcDecoder() {
#ifdef USE_FFMPEG
if (frame_) {
av_frame_free(&frame_);
}
if (codecCtx_) {
avcodec_free_context(&codecCtx_);
}
#endif
}
bool AvcDecoder::IsAvailable() {
#ifdef USE_FFMPEG
return true;
#else
return false;
#endif
}
void AvcDecoder::Flush() {
haveFrame_ = false;
#ifdef USE_FFMPEG
if (codecCtx_) {
avcodec_flush_buffers(codecCtx_);
}
if (frame_) {
av_frame_unref(frame_);
}
#endif
}
bool AvcDecoder::Decode(const u8 *data, int size) {
haveFrame_ = false;
#ifdef USE_FFMPEG
if (!codecCtx_ || !frame_ || !data || size <= 0) {
return false;
}
// ffmpeg reads a little past the end of a packet, so it gets a padded copy rather than the
// caller's buffer. A plain stack AVPacket, like the rest of the emulator uses - the heap
// packet API isn't in the ffmpeg headers every target builds against.
packetBuf_.resize((size_t)size + AV_INPUT_BUFFER_PADDING_SIZE);
memcpy(packetBuf_.data(), data, size);
memset(packetBuf_.data() + size, 0, AV_INPUT_BUFFER_PADDING_SIZE);
AVPacket packet;
av_init_packet(&packet);
packet.data = packetBuf_.data();
packet.size = size;
av_frame_unref(frame_);
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 48, 101)
int err = avcodec_send_packet(codecCtx_, &packet);
if (err == AVERROR(EAGAIN)) {
// The decoder has frames waiting and won't take more input until one is taken out. It did
// not consume the packet, so drain a frame and hand it the same packet again rather than
// dropping the access unit.
err = avcodec_receive_frame(codecCtx_, frame_);
if (err < 0) {
WARN_LOG(Log::ME, "AvcDecoder: send_packet wanted a frame out but none came (%d)", err);
return false;
}
const int resend = avcodec_send_packet(codecCtx_, &packet);
if (resend < 0) {
WARN_LOG(Log::ME, "AvcDecoder: send_packet failed on retry (%d)", resend);
return false;
}
// A frame is already in hand from the drain above - fall through to the format check.
} else {
if (err < 0) {
WARN_LOG(Log::ME, "AvcDecoder: send_packet failed (%d)", err);
return false;
}
err = avcodec_receive_frame(codecCtx_, frame_);
if (err == AVERROR(EAGAIN)) {
// Normal: H.264 reorders, so the decoder wants more input before it gives a frame back.
return false;
}
if (err < 0) {
WARN_LOG(Log::ME, "AvcDecoder: receive_frame failed (%d)", err);
return false;
}
}
#else
int gotFrame = 0;
int err = avcodec_decode_video2(codecCtx_, frame_, &gotFrame, &packet);
if (err < 0) {
WARN_LOG(Log::ME, "AvcDecoder: decode_video2 failed (%d)", err);
return false;
}
if (!gotFrame) {
// Normal: H.264 reorders, so the decoder wants more input before it gives a frame back.
return false;
}
#endif
width_ = frame_->width;
height_ = frame_->height;
haveFrame_ = width_ > 0 && height_ > 0;
return haveFrame_;
#else
return false;
#endif
}
const u8 *AvcDecoder::Plane(int index) const {
#ifdef USE_FFMPEG
if (!haveFrame_ || !frame_ || index < 0 || index >= 3) {
return nullptr;
}
return frame_->data[index];
#else
return nullptr;
#endif
}
int AvcDecoder::Stride(int index) const {
#ifdef USE_FFMPEG
if (!haveFrame_ || !frame_ || index < 0 || index >= 3) {
return 0;
}
return frame_->linesize[index];
#else
return 0;
#endif
}
+73
View File
@@ -0,0 +1,73 @@
// 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/.
#pragma once
#include <vector>
#include "Common/CommonTypes.h"
struct AVCodecContext;
struct AVFrame;
// A plain "hand it one access unit, get a frame back" H.264 decoder.
//
// This exists for sceVideocodec, which is the interface the Media Engine really exposes: the
// caller owns the buffers and hands over one access unit at a time. MediaEngine can't serve that
// - it is built around sceMpeg's own model, with the PSMF demuxer, the ringbuffer and its own
// frame pacing wrapped around the decoder - so this is deliberately a second, separate path
// rather than a refactor of code every game that plays video depends on. Merging the two is
// worth doing once sceVideocodec has proven itself.
//
// Output is whatever pixel format the decoder produced, normally YUV420P; the caller converts.
class AvcDecoder {
public:
AvcDecoder();
~AvcDecoder();
AvcDecoder(const AvcDecoder &) = delete;
AvcDecoder &operator=(const AvcDecoder &) = delete;
// Decodes one access unit. Returns true when a frame came out - H.264 reorders, so a frame
// is not produced for every unit fed in, and that is not an error.
bool Decode(const u8 *data, int size);
// Throws away any decoder state, for a seek or a new stream.
void Flush();
bool HasFrame() const { return haveFrame_; }
int Width() const { return width_; }
int Height() const { return height_; }
// Only meaningful while HasFrame(). Planes are Y, Cb, Cr; strides are in bytes and are not
// necessarily the width.
const u8 *Plane(int index) const;
int Stride(int index) const;
// True if we were built without ffmpeg, in which case nothing decodes.
static bool IsAvailable();
private:
AVCodecContext *codecCtx_ = nullptr;
AVFrame *frame_ = nullptr;
// The access unit, copied so we can pad it - ffmpeg reads a little past the end of a packet.
// Kept between calls so the usual case doesn't allocate.
std::vector<u8> packetBuf_;
bool haveFrame_ = false;
int width_ = 0;
int height_ = 0;
};
+2
View File
@@ -258,6 +258,7 @@
<ClInclude Include="..\..\Core\HW\Camera.h" />
<ClInclude Include="..\..\Core\HW\Display.h" />
<ClInclude Include="..\..\Core\HW\GpioMMIO.h" />
<ClInclude Include="..\..\Core\HW\AvcDecoder.h" />
<ClInclude Include="..\..\Core\HW\MediaEngine.h" />
<ClInclude Include="..\..\Core\HW\MemoryStick.h" />
<ClInclude Include="..\..\Core\HW\MpegDemux.h" />
@@ -541,6 +542,7 @@
<ClCompile Include="..\..\Core\HW\Camera.cpp" />
<ClCompile Include="..\..\Core\HW\Display.cpp" />
<ClCompile Include="..\..\Core\HW\GpioMMIO.cpp" />
<ClCompile Include="..\..\Core\HW\AvcDecoder.cpp" />
<ClCompile Include="..\..\Core\HW\MediaEngine.cpp" />
<ClCompile Include="..\..\Core\HW\MemoryStick.cpp" />
<ClCompile Include="..\..\Core\HW\MpegDemux.cpp" />
+2
View File
@@ -165,6 +165,7 @@
<ClCompile Include="..\..\Core\HW\Camera.cpp" />
<ClCompile Include="..\..\Core\HW\Display.cpp" />
<ClCompile Include="..\..\Core\HW\GpioMMIO.cpp" />
<ClCompile Include="..\..\Core\HW\AvcDecoder.cpp" />
<ClCompile Include="..\..\Core\HW\MediaEngine.cpp" />
<ClCompile Include="..\..\Core\HW\MemoryStick.cpp" />
<ClCompile Include="..\..\Core\HW\MpegDemux.cpp" />
@@ -524,6 +525,7 @@
<ClInclude Include="..\..\Core\HLE\sceAudio.h" />
<ClInclude Include="..\..\Core\HLE\sceAudiocodec.h" />
<ClInclude Include="..\..\Core\HLE\sceMpegbase.h" />
<ClInclude Include="..\..\Core\HW\AvcDecoder.h" />
<ClInclude Include="..\..\Core\HLE\sceAudioRouting.h" />
<ClInclude Include="..\..\Core\HLE\sceCcc.h" />
<ClInclude Include="..\..\Core\HLE\sceChnnlsv.h" />
+1
View File
@@ -614,6 +614,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/HW/GpioMMIO.cpp \
$(SRC)/Core/HW/MemoryStick.cpp \
$(SRC)/Core/HW/MpegDemux.cpp.arm \
$(SRC)/Core/HW/AvcDecoder.cpp.arm \
$(SRC)/Core/HW/MediaEngine.cpp.arm \
$(SRC)/Core/HW/SasAudio.cpp.arm \
$(SRC)/Core/HW/SasReverb.cpp.arm \
+1
View File
@@ -848,6 +848,7 @@ SOURCES_CXX += \
$(COREDIR)/HW/SimpleAudioDec.cpp \
$(COREDIR)/HW/Atrac3Standalone.cpp \
$(COREDIR)/HW/AsyncIOManager.cpp \
$(COREDIR)/HW/AvcDecoder.cpp \
$(COREDIR)/HW/MediaEngine.cpp \
$(COREDIR)/HW/MpegDemux.cpp \
$(COREDIR)/HW/MemoryStick.cpp \