From 3276cdf65f402f5825fbebde9c78bd4982f4f4f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 7 Sep 2026 16:56:29 -0600 Subject: [PATCH] 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) --- Core/CMakeLists.txt | 2 + Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 6 + Core/HW/AvcDecoder.cpp | 187 ++++++++++++++++++++++++++++ Core/HW/AvcDecoder.h | 73 +++++++++++ UWP/CoreUWP/CoreUWP.vcxproj | 2 + UWP/CoreUWP/CoreUWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + libretro/Makefile.common | 1 + 9 files changed, 276 insertions(+) create mode 100644 Core/HW/AvcDecoder.cpp create mode 100644 Core/HW/AvcDecoder.h diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index c6cf1f5a80..24b7c17d4d 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -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 diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index bdda9d35a3..b051ba598a 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -672,6 +672,7 @@ + @@ -1189,6 +1190,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 5133900c30..16ebfaddd9 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -357,6 +357,9 @@ HLE\Libraries + + HW + HW @@ -1692,6 +1695,9 @@ HLE\Libraries + + HW + HW diff --git a/Core/HW/AvcDecoder.cpp b/Core/HW/AvcDecoder.cpp new file mode 100644 index 0000000000..b87bb611be --- /dev/null +++ b/Core/HW/AvcDecoder.cpp @@ -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 +} diff --git a/Core/HW/AvcDecoder.h b/Core/HW/AvcDecoder.h new file mode 100644 index 0000000000..35cf1f7078 --- /dev/null +++ b/Core/HW/AvcDecoder.h @@ -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 + +#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 packetBuf_; + bool haveFrame_ = false; + int width_ = 0; + int height_ = 0; +}; diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index 9bda778ca2..291e398f04 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -258,6 +258,7 @@ + @@ -541,6 +542,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 555c850fcd..e800daa7e1 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -165,6 +165,7 @@ + @@ -524,6 +525,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 1a0d219d2c..72b9440a41 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -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 \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index a47b086c88..d865b0f66c 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -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 \